@contrast/agent 5.0.0-beta.9 → 5.0.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/LICENSE +1 -1
- package/README.md +151 -0
- package/lib/check-flag-vs-node-version.mjs +55 -0
- package/lib/esm-hooks.mjs +262 -0
- package/lib/esm-loader.mjs +149 -0
- package/lib/index.js +8 -1
- package/lib/initialize.mjs +132 -0
- package/package.json +13 -8
package/LICENSE
CHANGED
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 anaylsis 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:
|
|
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
|
|
3
|
+
"version": "5.0.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)",
|
|
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.
|
|
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.
|
|
21
|
-
"@contrast/architecture-components": "1.
|
|
22
|
-
"@contrast/assess": "1.
|
|
23
|
-
"@contrast/library-analysis": "1.
|
|
24
|
-
"@contrast/protect": "1.
|
|
25
|
-
"@contrast/route-coverage": "1.
|
|
24
|
+
"@contrast/agentify": "1.18.0",
|
|
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.0",
|
|
29
|
+
"@contrast/route-coverage": "1.14.0",
|
|
30
|
+
"@contrast/telemetry": "1.2.0",
|
|
26
31
|
"semver": "^7.3.7"
|
|
27
32
|
}
|
|
28
33
|
}
|