@contrast/esm-hooks 2.2.1 → 2.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/lib/common.mjs +136 -0
- package/lib/debug-methods.mjs +84 -0
- package/lib/hooks.mjs +163 -0
- package/lib/index.mjs +52 -108
- package/lib/loader-agent.mjs +78 -0
- package/lib/post-message/loader-client.mjs +58 -0
- package/lib/post-message/main-client.mjs +66 -0
- package/lib/post-message/send.mjs +25 -0
- package/package.json +3 -2
package/lib/common.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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 { readdir } from 'node:fs/promises';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
const REDIRECTS_PATH = './redirects';
|
|
20
|
+
|
|
21
|
+
export const mappings = await makeMappings();
|
|
22
|
+
|
|
23
|
+
// why do we need to fix paths? Because windows, node, and URLs don't get
|
|
24
|
+
// along very well.
|
|
25
|
+
//
|
|
26
|
+
// the load() hook receives an URL. When the URL is converted into an URL object,
|
|
27
|
+
// the pathname property will be '/C:/Users/.../redirects' on windows. And that
|
|
28
|
+
// doesn't start with a drive letter, so when fsp.readdir() gets it, node decides
|
|
29
|
+
// to add a drive letter: 'C:/C:/Users/.../redirects'. So we have to strip off the
|
|
30
|
+
// the leading / so the drive letter is recognized and another isn't added. it seems
|
|
31
|
+
// like a node bug (or possibly windows bug) but i don't have time to research the
|
|
32
|
+
// standards to figure out what URL.pathname should be in a windows file: context.
|
|
33
|
+
//
|
|
34
|
+
// maybe this should be in the load() hook?
|
|
35
|
+
//
|
|
36
|
+
// should
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
* @param {string} path
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function fixPath(p) {
|
|
43
|
+
if (p.match(/^\/[A-Z]:/)) {
|
|
44
|
+
p = p.slice(1);
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
import { getFileType } from './get-file-type.mjs';
|
|
50
|
+
export { getFileType };
|
|
51
|
+
|
|
52
|
+
async function makeMappings() {
|
|
53
|
+
/**
|
|
54
|
+
* @typedef { 'builtin' | 'commonjs' | 'module' } TargetType
|
|
55
|
+
* @typedef {string} RequireSpecifier
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* @type {Record<RequireSpecifier, {url: URL, target: TargetType}>}
|
|
59
|
+
*/
|
|
60
|
+
const mappings = Object.create(null);
|
|
61
|
+
|
|
62
|
+
// the name of the directory is the format of the target being loaded. so "cjs"
|
|
63
|
+
// means the target is a commonjs module that will be loaded via require(),
|
|
64
|
+
// "builtin" means the target is a builtin, and "esm" (when it's added) will
|
|
65
|
+
// mean that the target is a native esm module.
|
|
66
|
+
//
|
|
67
|
+
// at this time, all "builtin" modules are "cjs" modules and can be required.
|
|
68
|
+
// and all "cjs" modules are commonjs modules that can be required. The reason
|
|
69
|
+
// they need to be here is that they might be loaded by the ESM loader in the
|
|
70
|
+
// background thread, so we need to redirect them to the commonjs loader.
|
|
71
|
+
//
|
|
72
|
+
// all files in the "redirects" directory are .mjs files.
|
|
73
|
+
//
|
|
74
|
+
// 'esm' does not currently have any redirect files; there is one example file
|
|
75
|
+
// for node-fetch, but it does not have the extension .mjs, so it won't be used.
|
|
76
|
+
// 'esm' will be needed when we have to patch an esm-native file.
|
|
77
|
+
for (const dir of ['builtin', 'cjs', 'esm']) {
|
|
78
|
+
// keep track of recursive calls to handle nested directories, e.g., fs/promises
|
|
79
|
+
const pathStack = [];
|
|
80
|
+
|
|
81
|
+
// get an absolute path because reading the redirect file is going to be executed
|
|
82
|
+
// in another context.
|
|
83
|
+
const p = path.join(REDIRECTS_PATH, dir);
|
|
84
|
+
const redirectDir = new URL(p, import.meta.url);
|
|
85
|
+
const dirpath = fixPath(redirectDir.pathname);
|
|
86
|
+
|
|
87
|
+
await recursiveReaddir(dirpath);
|
|
88
|
+
|
|
89
|
+
// eslint-disable-next-line no-inner-declarations
|
|
90
|
+
async function recursiveReaddir(dirpath) {
|
|
91
|
+
const dirents = await readdir(dirpath, { withFileTypes: true });
|
|
92
|
+
for (const dirent of dirents) {
|
|
93
|
+
if (dirent.isDirectory()) {
|
|
94
|
+
const { name: subdir } = dirent;
|
|
95
|
+
const subp = path.join(dirpath, subdir);
|
|
96
|
+
pathStack.push(subdir);
|
|
97
|
+
await recursiveReaddir(subp);
|
|
98
|
+
pathStack.pop();
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!dirent.name.endsWith('.mjs')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// it a file that ends with .mjs, so it's a redirect file.
|
|
105
|
+
const redirectURL = new URL(path.join(p, ...pathStack, dirent.name), import.meta.url);
|
|
106
|
+
|
|
107
|
+
// all redirects point to .mjs files; the target property specifies the type of the
|
|
108
|
+
// the file that will be loaded by the .mjs. target. target is builtin, commonjs, or
|
|
109
|
+
// module.
|
|
110
|
+
// e.g., //'node-fetch': {url: new URL(`${p}/node-fetch.mjs`, import.meta.url), target: 'module'},
|
|
111
|
+
//
|
|
112
|
+
// there is a separate builtin directory because putting a colon in a filename doesn't work on windows. so
|
|
113
|
+
// that's why the mapping below adds the `node:` prefix.
|
|
114
|
+
let name = path.basename(dirent.name, '.mjs');
|
|
115
|
+
if (pathStack.length) {
|
|
116
|
+
name = `${pathStack.join('/')}/${name}`;
|
|
117
|
+
}
|
|
118
|
+
if (dir === 'builtin') {
|
|
119
|
+
mappings[name] = { url: redirectURL, target: 'builtin' };
|
|
120
|
+
mappings[`node:${name}`] = { url: redirectURL, target: 'builtin' };
|
|
121
|
+
} else if (dir === 'cjs') {
|
|
122
|
+
mappings[name] = { url: redirectURL, target: 'commonjs' };
|
|
123
|
+
} else if (dir === 'esm') {
|
|
124
|
+
mappings[name] = { url: redirectURL, target: 'module' };
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`target type ${dir} not yet implemented`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return mappings;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// useful for tests
|
|
136
|
+
export { makeMappings };
|
|
@@ -0,0 +1,84 @@
|
|
|
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 * as process from 'node:process';
|
|
18
|
+
import { threadId as tid } from 'node:worker_threads';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_OPTS = { mask: process.env.CSI_HOOKS_LOG, install: true };
|
|
21
|
+
const LOG_LOAD = 1;
|
|
22
|
+
const LOG_RESOLVE = 2;
|
|
23
|
+
const LOG_REQUIRE_ALL = 4;
|
|
24
|
+
|
|
25
|
+
export default function init(core, opts = DEFAULT_OPTS) {
|
|
26
|
+
const { esmHooks } = core;
|
|
27
|
+
|
|
28
|
+
if (!opts.mask) return;
|
|
29
|
+
const LOG = +opts.mask;
|
|
30
|
+
|
|
31
|
+
Object.assign(esmHooks, {
|
|
32
|
+
_rawDebug(value) {
|
|
33
|
+
process._rawDebug(value);
|
|
34
|
+
},
|
|
35
|
+
debugCjsCompile(filename) {
|
|
36
|
+
(LOG & LOG_REQUIRE_ALL) && esmHooks._rawDebug(`CJS(${tid}) _compile() ${filename}`);
|
|
37
|
+
},
|
|
38
|
+
debugCjsExtensions(filename) {
|
|
39
|
+
(LOG & LOG_REQUIRE_ALL) && esmHooks._rawDebug(`CJS(${tid}) extensions.js() ${filename}`);
|
|
40
|
+
},
|
|
41
|
+
debugCjsLoad(request) {
|
|
42
|
+
(LOG & LOG_LOAD) && esmHooks._rawDebug(`CJS(${tid}) _load() ${request}`);
|
|
43
|
+
},
|
|
44
|
+
debugCjsRequire(moduleId) {
|
|
45
|
+
(LOG & LOG_REQUIRE_ALL) && esmHooks._rawDebug(`CJS(${tid}) require() ${moduleId}`);
|
|
46
|
+
},
|
|
47
|
+
debugEsmInitialize(specifier) {
|
|
48
|
+
LOG && esmHooks._rawDebug(`ESM(${tid}) initialize() ${specifier}`);
|
|
49
|
+
},
|
|
50
|
+
debugEsmResolve(specifier) {
|
|
51
|
+
(LOG & LOG_RESOLVE) && esmHooks._rawDebug(`ESM(${tid}) resolve() ${specifier}`);
|
|
52
|
+
},
|
|
53
|
+
debugEsmLoad(url) {
|
|
54
|
+
(LOG & LOG_LOAD) && esmHooks._rawDebug(`ESM(${tid}) load() ${url}`);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (opts.install) {
|
|
59
|
+
const originalRequire = Module.prototype.require;
|
|
60
|
+
Module.prototype.require = function(moduleId) {
|
|
61
|
+
esmHooks.debugCjsRequire(moduleId);
|
|
62
|
+
return originalRequire.call(this, moduleId);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const originalCompile = Module.prototype._compile;
|
|
66
|
+
Module.prototype._compile = function(code, filename) {
|
|
67
|
+
esmHooks.debugCjsCompile(filename);
|
|
68
|
+
return originalCompile.call(this, code, filename);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const originalExtensions = Module._extensions['.js'];
|
|
72
|
+
Module._extensions['.js'] = function(module, filename) {
|
|
73
|
+
esmHooks.debugCjsExtensions(filename);
|
|
74
|
+
return originalExtensions.call(this, module, filename);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const originalLoad = Module._load;
|
|
78
|
+
Module._load = function(request, parent, isMain) {
|
|
79
|
+
esmHooks.debugCjsLoad(request);
|
|
80
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return esmHooks;
|
|
84
|
+
}
|
package/lib/hooks.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
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 { readFile as rf } from 'node:fs/promises';
|
|
17
|
+
import * as process from 'node:process';
|
|
18
|
+
import { fixPath, getFileType, mappings } from './common.mjs';
|
|
19
|
+
import { default as initLoaderAgent } from './loader-agent.mjs';
|
|
20
|
+
const [major, minor] = process.versions.node.split('.').map(it => +it);
|
|
21
|
+
const isLT16_12 = major < 16 || (major === 16 && minor < 12);
|
|
22
|
+
|
|
23
|
+
const readFile = rf;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Agent instance with minimum footprint that handles functionality related esm module loading.
|
|
27
|
+
* - We handles redirects to force require.
|
|
28
|
+
* - Module rewriting via exported load hook
|
|
29
|
+
*/
|
|
30
|
+
let loaderAgent;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{
|
|
34
|
+
* modes: string[],
|
|
35
|
+
* port: import('node:worker_threads').MessagePort,
|
|
36
|
+
* appInfo: import('@contrast/common').AppInfo,
|
|
37
|
+
* agentVersion: string,
|
|
38
|
+
* }} data
|
|
39
|
+
*/
|
|
40
|
+
async function initialize(data = {}) {
|
|
41
|
+
loaderAgent = initLoaderAgent(data);
|
|
42
|
+
loaderAgent.esmHooks.debugEsmInitialize?.();
|
|
43
|
+
await loaderAgent.install();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function resolve(specifier, context, nextResolve) {
|
|
47
|
+
loaderAgent?.esmHooks?.debugEsmResolve?.(specifier);
|
|
48
|
+
|
|
49
|
+
let isFlaggedToPatch = false;
|
|
50
|
+
if (context.parentURL) {
|
|
51
|
+
isFlaggedToPatch = context.parentURL.endsWith('csi-flag=', context.parentURL.length - 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (loaderAgent?.enable && !isFlaggedToPatch && specifier in mappings) {
|
|
55
|
+
// eslint-disable-next-line prefer-const
|
|
56
|
+
let { url, format, target } = mappings[specifier];
|
|
57
|
+
// set flag to module or commonjs. i'm not sure this is needed but am keeping it
|
|
58
|
+
// in place until we have to implement esm-native module rewrites/wrapping. this
|
|
59
|
+
// is the point at which resolve() communicates to load().
|
|
60
|
+
//
|
|
61
|
+
// builtin's are probably the most likely to be loaded, so they're first.
|
|
62
|
+
// some tweaks might be needed when we start to patch esm-native modules
|
|
63
|
+
// in esm-native-code:
|
|
64
|
+
// https://nodejs.org/docs/latest-v20.x/api/esm.html#builtin-modules
|
|
65
|
+
// https://nodejs.org/docs/latest-v20.x/api/module.html#modulesyncbuiltinesmexports
|
|
66
|
+
if (target === 'builtin') {
|
|
67
|
+
url = `${url.href}?csi-flag=m`;
|
|
68
|
+
format = 'module';
|
|
69
|
+
} else if (target === 'commonjs') {
|
|
70
|
+
url = `${url.href}?csi-flag=c`;
|
|
71
|
+
format = getFileType(url) || format || 'commonjs';
|
|
72
|
+
} else if (target === 'module') {
|
|
73
|
+
url = `${url.href}?csi-flag=m`;
|
|
74
|
+
format = getFileType(url) || format || 'module';
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`unexpected target ${target} for ${specifier}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
url,
|
|
81
|
+
format,
|
|
82
|
+
shortCircuit: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return protectedNextResolve(nextResolve, specifier, context);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function load(url, context, nextLoad) {
|
|
90
|
+
loaderAgent?.esmHooks?.debugEsmLoad?.(url);
|
|
91
|
+
|
|
92
|
+
const urlObject = new URL(url);
|
|
93
|
+
|
|
94
|
+
if (loaderAgent?.enable && urlObject.searchParams.has('csi-flag')) {
|
|
95
|
+
const { pathname } = urlObject;
|
|
96
|
+
const source = await readFile(fixPath(pathname), 'utf8');
|
|
97
|
+
return {
|
|
98
|
+
source,
|
|
99
|
+
format: 'module',
|
|
100
|
+
shortCircuit: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (urlObject.pathname.endsWith('.node')) {
|
|
105
|
+
const metaUrl = JSON.stringify(url);
|
|
106
|
+
const addon = urlObject.pathname;
|
|
107
|
+
return {
|
|
108
|
+
source: `require("node:module").createRequire(${metaUrl})("${addon}")`,
|
|
109
|
+
format: 'commonjs',
|
|
110
|
+
shortCircuit: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// if it's not a builtin, a .node addon, or a flagged file, it needs to be
|
|
115
|
+
// rewritten if it's a module.
|
|
116
|
+
if (urlObject.pathname.match(/(.js|.mjs|.cjs)$/)) {
|
|
117
|
+
// if it's not a module it will be rewritten by the require hooks.
|
|
118
|
+
const type = getFileType(urlObject);
|
|
119
|
+
if (type !== 'module') {
|
|
120
|
+
return nextLoad(url, context);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const filename = fixPath(urlObject.pathname);
|
|
124
|
+
const source = await readFile(filename, 'utf8');
|
|
125
|
+
let result;
|
|
126
|
+
|
|
127
|
+
if (loaderAgent?.enable) {
|
|
128
|
+
const rewriteOptions = {
|
|
129
|
+
filename,
|
|
130
|
+
isModule: type === 'module',
|
|
131
|
+
inject: true,
|
|
132
|
+
wrap: type !== 'module', // cannot wrap modules
|
|
133
|
+
};
|
|
134
|
+
result = await loaderAgent.rewriter.rewrite(source, rewriteOptions);
|
|
135
|
+
|
|
136
|
+
if (process.env.CSI_EXPOSE_CORE) {
|
|
137
|
+
// only do this in testing scenarios (todo: compose this functionality into test agents instead)
|
|
138
|
+
loaderAgent.esmHooks.portClient.sendStatus('rewriting file during ESM load hook', rewriteOptions);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
source: result?.code || source,
|
|
144
|
+
format: type || 'commonjs', // don't know what else to do here. log?
|
|
145
|
+
shortCircuit: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// this never gets called, so hmmm.
|
|
150
|
+
return nextLoad(url, context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function protectedNextResolve(nextResolve, specifier, context) {
|
|
154
|
+
if (context.parentURL) {
|
|
155
|
+
if (context.conditions.at(-1) === 'node-addons' || context.importAssertions || isLT16_12) {
|
|
156
|
+
return nextResolve(specifier, context);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return nextResolve(specifier);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { initialize, resolve, load };
|
package/lib/index.mjs
CHANGED
|
@@ -13,121 +13,65 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
import Module from 'node:module';
|
|
17
|
+
import { MessageChannel } from 'node:worker_threads';
|
|
18
|
+
import { default as debugMethods } from './debug-methods.mjs';
|
|
19
|
+
import { initialize, load, resolve } from './hooks.mjs';
|
|
20
|
+
import { default as portClient } from './post-message/main-client.mjs';
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
export default function init(core) {
|
|
23
|
+
const {
|
|
24
|
+
port1: mainPort,
|
|
25
|
+
port2: workerPort
|
|
26
|
+
} = new MessageChannel();
|
|
22
27
|
|
|
23
|
-
const
|
|
28
|
+
const esmHooks = core.esmHooks = {
|
|
29
|
+
hooks: { resolve, load }, // remove when all LTS versions support Module.register
|
|
30
|
+
getActiveModes() {
|
|
31
|
+
const modes = [];
|
|
32
|
+
if (core.config.getEffectiveValue('assess.enable')) {
|
|
33
|
+
modes.push('assess');
|
|
34
|
+
}
|
|
35
|
+
if (core.config.getEffectiveValue('protect.enable')) {
|
|
36
|
+
modes.push('protect');
|
|
37
|
+
}
|
|
38
|
+
return modes;
|
|
39
|
+
},
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// maybe this should be in the load() hook?
|
|
37
|
-
//
|
|
41
|
+
/**
|
|
42
|
+
* Install AFTER we have onboarded with contrast-ui. This way we will have
|
|
43
|
+
* effective values for whether to enable protect/assess.
|
|
44
|
+
*/
|
|
45
|
+
async install() {
|
|
46
|
+
const data = {
|
|
47
|
+
port: workerPort,
|
|
48
|
+
modes: esmHooks.getActiveModes(),
|
|
49
|
+
appInfo: core.appInfo,
|
|
50
|
+
agentVersion: core.agentVersion,
|
|
51
|
+
};
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
// Instantiate loader agent via register (if available) or manually.
|
|
54
|
+
// we can simplify when all LTS versions support register
|
|
55
|
+
if (Module.register) {
|
|
56
|
+
await Module.register(new URL('./hooks.mjs', import.meta.url).href, {
|
|
57
|
+
data,
|
|
58
|
+
transferList: [workerPort],
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
await initialize(data);
|
|
62
|
+
}
|
|
40
63
|
|
|
41
|
-
//
|
|
42
|
-
|
|
64
|
+
// these have side-effects, so compose during installation phase
|
|
65
|
+
debugMethods(core);
|
|
66
|
+
},
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
getFileType,
|
|
68
|
+
async uninstall() {
|
|
69
|
+
// inform the loader agent it needs to disable and stop rewrites etc
|
|
70
|
+
esmHooks.portClient?.sendRPC?.('disable');
|
|
71
|
+
},
|
|
49
72
|
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function makeMappings() {
|
|
53
|
-
/**
|
|
54
|
-
* @typedef { 'builtin' | 'commonjs' | 'module' } TargetType
|
|
55
|
-
* @typedef {string} RequireSpecifier
|
|
56
|
-
*/
|
|
57
|
-
/**
|
|
58
|
-
* @type {Record<RequireSpecifier, {url: URL, target: TargetType}>}
|
|
59
|
-
*/
|
|
60
|
-
const mappings = Object.create(null);
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
// means the target is a commonjs module that will be loaded via require(),
|
|
64
|
-
// "builtin" means the target is a builtin, and "esm" (when it's added) will
|
|
65
|
-
// mean that the target is a native esm module.
|
|
66
|
-
//
|
|
67
|
-
// at this time, all "builtin" modules are "cjs" modules and can be required.
|
|
68
|
-
// and all "cjs" modules are commonjs modules that can be required. The reason
|
|
69
|
-
// they need to be here is that they might be loaded by the ESM loader in the
|
|
70
|
-
// background thread, so we need to redirect them to the commonjs loader.
|
|
71
|
-
//
|
|
72
|
-
// all files in the "redirects" directory are .mjs files.
|
|
73
|
-
//
|
|
74
|
-
// 'esm' does not currently have any redirect files; there is one example file
|
|
75
|
-
// for node-fetch, but it does not have the extension .mjs, so it won't be used.
|
|
76
|
-
// 'esm' will be needed when we have to patch an esm-native file.
|
|
77
|
-
for (const dir of ['builtin', 'cjs', 'esm']) {
|
|
78
|
-
// keep track of recursive calls to handle nested directories, e.g., fs/promises
|
|
79
|
-
const pathStack = [];
|
|
80
|
-
|
|
81
|
-
// get an absolute path because reading the redirect file is going to be executed
|
|
82
|
-
// in another context.
|
|
83
|
-
const p = path.join(redirectPath, dir);
|
|
84
|
-
const redirectDir = new URL(p, import.meta.url);
|
|
85
|
-
const dirpath = fixPath(redirectDir.pathname);
|
|
86
|
-
|
|
87
|
-
await recursiveReaddir(dirpath);
|
|
88
|
-
|
|
89
|
-
// eslint-disable-next-line no-inner-declarations
|
|
90
|
-
async function recursiveReaddir(dirpath) {
|
|
91
|
-
const dirents = await readdir(dirpath, { withFileTypes: true });
|
|
92
|
-
for (const dirent of dirents) {
|
|
93
|
-
if (dirent.isDirectory()) {
|
|
94
|
-
const { name: subdir } = dirent;
|
|
95
|
-
const subp = path.join(dirpath, subdir);
|
|
96
|
-
pathStack.push(subdir);
|
|
97
|
-
await recursiveReaddir(subp);
|
|
98
|
-
pathStack.pop();
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (!dirent.name.endsWith('.mjs')) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
// it a file that ends with .mjs, so it's a redirect file.
|
|
105
|
-
const redirectURL = new URL(path.join(p, ...pathStack, dirent.name), import.meta.url);
|
|
106
|
-
|
|
107
|
-
// all redirects point to .mjs files; the target property specifies the type of the
|
|
108
|
-
// the file that will be loaded by the .mjs. target. target is builtin, commonjs, or
|
|
109
|
-
// module.
|
|
110
|
-
// e.g., //'node-fetch': {url: new URL(`${p}/node-fetch.mjs`, import.meta.url), target: 'module'},
|
|
111
|
-
//
|
|
112
|
-
// there is a separate builtin directory because putting a colon in a filename doesn't work on windows. so
|
|
113
|
-
// that's why the mapping below adds the `node:` prefix.
|
|
114
|
-
let name = path.basename(dirent.name, '.mjs');
|
|
115
|
-
if (pathStack.length) {
|
|
116
|
-
name = `${pathStack.join('/')}/${name}`;
|
|
117
|
-
}
|
|
118
|
-
if (dir === 'builtin') {
|
|
119
|
-
mappings[name] = { url: redirectURL, target: 'builtin' };
|
|
120
|
-
mappings[`node:${name}`] = { url: redirectURL, target: 'builtin' };
|
|
121
|
-
} else if (dir === 'cjs') {
|
|
122
|
-
mappings[name] = { url: redirectURL, target: 'commonjs' };
|
|
123
|
-
} else if (dir === 'esm') {
|
|
124
|
-
mappings[name] = { url: redirectURL, target: 'module' };
|
|
125
|
-
} else {
|
|
126
|
-
throw new Error(`target type ${dir} not yet implemented`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
74
|
+
portClient(core, { port: mainPort });
|
|
131
75
|
|
|
132
|
-
return
|
|
76
|
+
return esmHooks;
|
|
133
77
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { IntentionalError } from '@contrast/common';
|
|
18
|
+
import { default as debugMethods } from './debug-methods.mjs';
|
|
19
|
+
import { default as portClient } from './post-message/loader-client.mjs';
|
|
20
|
+
const require = Module.createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
// TODO: language
|
|
23
|
+
const ERROR_MESSAGE = 'An error prevented the Contrast agent from initializing in the loader thread.';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {{
|
|
27
|
+
* modes: string[],
|
|
28
|
+
* port: import('node:worker_threads').MessagePort,
|
|
29
|
+
* appInfo: import('@contrast/common').AppInfo,
|
|
30
|
+
* agentVersion: string,
|
|
31
|
+
* }} data
|
|
32
|
+
*/
|
|
33
|
+
export default function init({ appInfo, agentVersion, port, modes }) {
|
|
34
|
+
const core = {
|
|
35
|
+
appInfo,
|
|
36
|
+
agentVersion,
|
|
37
|
+
// this will toggle functionality in hooks.mjs e.g. redirects/rewriting
|
|
38
|
+
enable: true,
|
|
39
|
+
// stub for debugMethods
|
|
40
|
+
esmHooks: {},
|
|
41
|
+
async install() {
|
|
42
|
+
if (!modes?.length) {
|
|
43
|
+
throw new Error('worker agent invalid state: no modes');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const mode of modes) {
|
|
47
|
+
core.rewriter.install(mode);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
debugMethods(core);
|
|
51
|
+
core.esmHooks?.portClient?.sendStatus?.('initialized and installed esm loader agent');
|
|
52
|
+
},
|
|
53
|
+
uninstall() {
|
|
54
|
+
core.enable = false;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
require('@contrast/core/lib/messages')(core);
|
|
60
|
+
require('@contrast/config')(core);
|
|
61
|
+
require('@contrast/logger').default(core);
|
|
62
|
+
require('@contrast/rewriter')(core);
|
|
63
|
+
portClient(core, { port });
|
|
64
|
+
|
|
65
|
+
return core;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
core.enable = false;
|
|
68
|
+
|
|
69
|
+
// ignore intentional errors
|
|
70
|
+
if (!(err instanceof IntentionalError)) {
|
|
71
|
+
if (core.logger) {
|
|
72
|
+
core.logger.error({ err }, ERROR_MESSAGE);
|
|
73
|
+
} else {
|
|
74
|
+
console.error(new Error(ERROR_MESSAGE, { cause: err }));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { Event } from '@contrast/common';
|
|
17
|
+
import { send } from './send.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles loader agent communication with the main agent's port client.
|
|
21
|
+
* Has API to send status messages to main agent e.g. for when it installs/uninstalls.
|
|
22
|
+
* Also listens for for TS settings updates and messages from main agent instructing it to disable.
|
|
23
|
+
* @param {any} core
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {import('node:worker_threads').MessagePort} opts.port
|
|
26
|
+
*/
|
|
27
|
+
export default function init(core, { port }) {
|
|
28
|
+
const portClient = core.esmHooks.portClient = {
|
|
29
|
+
_port: port,
|
|
30
|
+
_handlers: {
|
|
31
|
+
disable() {
|
|
32
|
+
core.uninstall?.();
|
|
33
|
+
portClient.sendStatus('loader agent has been uninstalled');
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
sendStatus(msg, data) {
|
|
37
|
+
send(port, { type: 'status', msg, data });
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
port.on('message', (raw) => {
|
|
42
|
+
if (raw?.type === Event.SERVER_SETTINGS_UPDATE) {
|
|
43
|
+
// forward main agent settings updates to loader components
|
|
44
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (raw?.type === 'rpc') {
|
|
48
|
+
// dispatch appropriately
|
|
49
|
+
portClient._handlers[raw.action]?.(raw.data);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// do this after 'message' listeners are added, otherwise that process will automatically ref again
|
|
54
|
+
// https://nodejs.org/api/worker_threads.html#portunref
|
|
55
|
+
port.unref();
|
|
56
|
+
|
|
57
|
+
return portClient;
|
|
58
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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 { Event } from '@contrast/common';
|
|
17
|
+
import { send } from './send.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handles main agent communication with the loader agent's port client.
|
|
21
|
+
* Has API to forward events, and for sending RPC-like messages e.g. for instructing it to disable.
|
|
22
|
+
* Will listen for status messages from the loader agent and log them.
|
|
23
|
+
* @param {any} core
|
|
24
|
+
* @param {Object} opts
|
|
25
|
+
* @param {import('node:worker_threads').MessagePort} opts.port
|
|
26
|
+
*/
|
|
27
|
+
export default function init(core, { port }) {
|
|
28
|
+
const portClient = core.esmHooks.portClient = {
|
|
29
|
+
_port: port,
|
|
30
|
+
_handlers: {
|
|
31
|
+
status(msg, data) {
|
|
32
|
+
core.logger.trace({ name: 'contrast:esm:loader', data }, msg);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Forward events e.g. SERVER_SETTINGS_UPDATE
|
|
37
|
+
*/
|
|
38
|
+
sendEvent(event, data) {
|
|
39
|
+
send(port, { type: event, ...data });
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Tell the loader agent to do something e.g. disable/uninstall
|
|
43
|
+
*/
|
|
44
|
+
sendRPC(action, data) {
|
|
45
|
+
send(port, { type: 'rpc', action, data });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// handling messages from loader agent
|
|
50
|
+
portClient._port.on('message', (raw) => {
|
|
51
|
+
if (raw?.type == 'status') {
|
|
52
|
+
portClient._handlers.status(raw.msg, raw.data);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// forward settings updates to loader agent via port
|
|
57
|
+
core.messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
|
|
58
|
+
portClient.sendEvent(Event.SERVER_SETTINGS_UPDATE, msg);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// do this after 'message' listeners are added, otherwise that process will automatically ref again
|
|
62
|
+
// https://nodejs.org/api/worker_threads.html#portunref
|
|
63
|
+
port.unref();
|
|
64
|
+
|
|
65
|
+
return portClient;
|
|
66
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 W from 'node:worker_threads';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wraps postMessage to handle generic message-sending activities e.g. enriching messages with tid.
|
|
20
|
+
* @param {W.MessagePort} port
|
|
21
|
+
* @param {any} data
|
|
22
|
+
*/
|
|
23
|
+
export function send(port, data) {
|
|
24
|
+
port.postMessage({ tid: W.threadId, ...data });
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/esm-hooks",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Support for loading and instrumenting ECMAScript modules",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"test": "../scripts/test.sh"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@contrast/
|
|
22
|
+
"@contrast/common": "1.19.0",
|
|
23
|
+
"@contrast/core": "1.30.0",
|
|
23
24
|
"@contrast/find-package-json": "^1.0.0"
|
|
24
25
|
}
|
|
25
26
|
}
|