@contrast/agentify 1.20.1 → 1.22.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.
@@ -78,7 +78,7 @@ module.exports = function (deps) {
78
78
  // cannot parse lone function expressions that are not part of an assignment.
79
79
  try {
80
80
  let unwritten = functionHooks.contextualizeFunction(code);
81
- unwritten = rewriter.unwrite(unwritten);
81
+ unwritten = rewriter.unwriteSync(unwritten);
82
82
  unwritten = unwritten.replace(METHOD_CONTEXT, '');
83
83
  unwritten = unwritten.replace(FUNCTION_CONTEXT, '');
84
84
  unwritten = unwritten.replace(/;\s*$/, ''); // removes trailing semicolon/whitespace
package/lib/index.d.ts CHANGED
@@ -13,21 +13,36 @@
13
13
  * way not consistent with the End User License Agreement.
14
14
  */
15
15
 
16
- import { Config } from '@contrast/config';
17
16
  import { Installable } from '@contrast/common';
17
+ import { Config } from '@contrast/config';
18
+ import { Core as _Core } from '@contrast/core';
18
19
  import { Logger } from '@contrast/logger';
19
20
  import { Rewriter } from '@contrast/rewriter';
20
21
  import RequireHook from '@contrast/require-hook';
22
+ import { Patcher } from '@contrast/patcher';
23
+ import { Scopes } from '@contrast/scopes';
24
+ import { Deadzones } from '@contrast/deadzones';
25
+ import { ReporterBus } from '@contrast/reporter';
21
26
 
22
27
  declare module 'module' {
23
28
  class Module {
24
29
  /**
30
+ * @see https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js
31
+ * @param content The source code of the module
32
+ * @param filename The file path of the module
33
+ */
34
+ _compile(content: string, filename: string);
35
+
36
+ static _extensions: {
37
+ /**
25
38
  * @see https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js
26
- * @param content The source code of the module
39
+ * @param module The module to compile
27
40
  * @param filename The file path of the module
28
41
  */
29
- _compile(content: string, filename: string);
42
+ ['.js'](module: import('module'), filename: string);
43
+ };
30
44
  }
45
+
31
46
  export = Module;
32
47
  }
33
48
 
@@ -36,13 +51,17 @@ declare module 'node:module' {
36
51
  export = Module;
37
52
  }
38
53
 
39
- // TODO: this is now much larger with all of the existing core deps
40
- export interface Core {
41
- readonly config: Config;
42
- readonly depHooks: RequireHook;
43
- readonly logger: Logger;
44
- readonly rewriter: Rewriter;
45
- readonly threadInfo: any;
54
+ export interface Core extends _Core {
55
+ config: Config;
56
+ logger: Logger;
57
+ depHooks: RequireHook;
58
+ patcher: Patcher
59
+ rewriter: Rewriter;
60
+ scopes: Scopes;
61
+ deadzones: Deadzones;
62
+ reporter: ReporterBus;
63
+ instrumentation: any;
64
+ metrics: any;
46
65
  }
47
66
 
48
67
  export interface AgentifyOptions {
@@ -50,13 +69,13 @@ export interface AgentifyOptions {
50
69
  }
51
70
 
52
71
  export interface PreRunMain {
53
- (core: any): Installable | void | Promise<Installable | void>;
72
+ (core: Core): Installable | void | Promise<Installable | void>;
54
73
  }
55
74
 
56
75
  export interface Agentify {
57
- (preRunMain: PreRunMain, opts?: AgentifyOptions): any;
76
+ (preRunMain: PreRunMain, opts?: AgentifyOptions): Installable | void;
58
77
  }
59
78
 
60
- declare function init(core: any): Agentify;
79
+ declare function init(core: Partial<Core>): Agentify;
61
80
 
62
81
  export = init;
package/lib/index.js CHANGED
@@ -19,6 +19,14 @@ const Module = require('module');
19
19
  const { IntentionalError } = require('@contrast/common');
20
20
 
21
21
  const ERROR_MESSAGE = 'An error prevented the Contrast agent from installing. The application will be run without instrumentation.';
22
+ /**
23
+ * Specific agents may want to add their startup validation to this list as needed.
24
+ * We have to install the reporter first since installation is contingent on TS onboarding responses.
25
+ * Otherwise, the order of installing side effects doesn't matter too much. Ideally, the installations
26
+ * that most affect the environment and "normal" execution of things should be done lastly e.g. modifying prototypes.
27
+ * A good rule might be to install in the order such that side-effects that are easier to undo should be done first.
28
+ * With that rule in mind, ESM support is the most complex (loader agent) so it should come last.
29
+ */
22
30
  const DEFAULT_INSTALL_ORDER = [
23
31
  'reporter',
24
32
  'contrastMethods',
@@ -32,16 +40,89 @@ const DEFAULT_INSTALL_ORDER = [
32
40
  'routeCoverage',
33
41
  'libraryAnalysis',
34
42
  'heapSnapshots',
43
+ 'metrics',
35
44
  'rewriteHooks',
36
45
  'functionHooks',
37
- 'metrics',
46
+ 'esmHooks',
38
47
  ];
39
48
 
40
49
  /**
50
+ * Utility for making agents: entrypoints indended to be run via --require, --loader, --import.
51
+ * Composes all needed dependencies during factory instantiation, and installs various components
52
+ * that alter an application's environment in order for composed features to operate e.g. request scopes,
53
+ * source code rewrite hooks etc.
41
54
  * @param {any} core
42
55
  * @returns {import('.').Agentify}
43
56
  */
44
57
  module.exports = function init(core = {}) {
58
+ core.startTime = process.hrtime.bigint();
59
+
60
+ let _callback;
61
+ let _opts;
62
+
63
+ core.agentify = async function agentify(callback, opts = {}) {
64
+ _callback = callback;
65
+ _opts = opts;
66
+ _opts.type = _opts.type ?? 'cjs';
67
+ _opts.installOrder = _opts.installOrder ?? DEFAULT_INSTALL_ORDER;
68
+
69
+ // for 'cjs' we hook runMain and install prior to running it
70
+ if (_opts.type === 'cjs') {
71
+ if (typeof callback !== 'function') {
72
+ throw new Error('Invalid usage: first argument must be a function');
73
+ }
74
+ const { runMain } = Module;
75
+ /**
76
+ * This is one side effect that will always occur, even with `installOrder: []`.
77
+ * The act of calling `agentify()` is enough to cause this side effect, by design.
78
+ */
79
+ Module.runMain = async function (...args) {
80
+ await install();
81
+ return runMain.apply(this, args);
82
+ };
83
+ } else {
84
+ // for 'esm' we load esm-hooks support, which will install lastly
85
+ const { default: esmHooks } = await import('@contrast/esm-hooks/lib/index.mjs');
86
+ esmHooks(core);
87
+
88
+ await install();
89
+ }
90
+
91
+ return core;
92
+ };
93
+
94
+ async function install() {
95
+ const { config, logger } = core;
96
+
97
+ try {
98
+ for (const err of config._errors) {
99
+ throw err; // move this into config itself since we now handle errors
100
+ }
101
+
102
+ logger.info('Starting %s v%s', core.agentName, core.agentVersion);
103
+ logger.info({ config }, 'Agent configuration');
104
+
105
+ const plugin = await _callback?.(core);
106
+
107
+ for (const svcName of _opts.installOrder ?? []) {
108
+ const svc = core[svcName];
109
+ if (svc?.install) {
110
+ logger.trace('installing service: %s', svcName);
111
+ await svc.install();
112
+ }
113
+ }
114
+
115
+ if (plugin?.install) {
116
+ await plugin.install();
117
+ }
118
+
119
+ core.logDiagnosticFiles(); // should this be moved into a separate install side-effect?
120
+ } catch (err) {
121
+ // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
122
+ logger.error({ err }, ERROR_MESSAGE);
123
+ }
124
+ }
125
+
45
126
  try {
46
127
  require('@contrast/core/lib/messages')(core);
47
128
  require('@contrast/config')(core);
@@ -49,8 +130,6 @@ module.exports = function init(core = {}) {
49
130
  throw core.config._errors[0];
50
131
  }
51
132
  require('@contrast/logger').default(core);
52
-
53
- // @contrast/info ?
54
133
  require('@contrast/core/lib/agent-info')(core);
55
134
  require('@contrast/core/lib/system-info')(core);
56
135
  require('@contrast/core/lib/app-info')(core);
@@ -62,77 +141,20 @@ module.exports = function init(core = {}) {
62
141
  require('@contrast/dep-hooks')(core);
63
142
  require('@contrast/patcher')(core);
64
143
  require('@contrast/core/lib/capture-stacktrace')(core);
65
-
66
144
  require('@contrast/rewriter')(core); // merge contrast-methods?
67
145
  require('@contrast/core/lib/contrast-methods')(core); // can we remove dependency on patcher?
68
-
69
146
  require('@contrast/scopes')(core);
70
147
  require('@contrast/deadzones')(core);
71
148
  require('@contrast/reporter').default(core);
72
149
  require('@contrast/instrumentation')(core);
73
150
  require('@contrast/metrics')(core);
74
151
 
75
- // compose add'l local services
152
+ // compose additional local services
76
153
  require('./heap-snapshots')(core);
77
154
  require('./sources')(core);
78
155
  require('./function-hooks')(core);
79
156
  require('./log-diagnostic-files')(core); // this doesn't really belong in agentify
80
157
  require('./rewrite-hooks')(core);
81
-
82
- /**
83
- * The interface is a function, which when called, will hook runMain
84
- * @type {import('.').Agentify}
85
- */
86
- core.agentify = function agentify(
87
- preRunMain,
88
- { installOrder = DEFAULT_INSTALL_ORDER } = {},
89
- ) {
90
- if (typeof preRunMain !== 'function') {
91
- throw new Error('Invalid usage: first argument must be a function');
92
- }
93
-
94
- const { runMain } = Module;
95
- const { config, logger } = core;
96
-
97
- /**
98
- * This is one side effect that will always occur, even with `installOrder: []`.
99
- * The act of calling `agentify()` is enough to cause this side effect, by design.
100
- */
101
- Module.runMain = async function (...args) {
102
- try {
103
- for (const err of config._errors) {
104
- throw err; // move this into config itself since we now handle errors
105
- }
106
-
107
- logger.info('Starting %s v%s', core.agentName, core.agentVersion);
108
- // pino serializers know how to log redacted values and omit certain props
109
- logger.info({ config }, 'Agent configuration');
110
-
111
- const plugin = await preRunMain(core);
112
-
113
- for (const svcName of installOrder ?? []) {
114
- const svc = core[svcName];
115
- if (svc?.install) {
116
- logger.trace('installing service: %s', svcName);
117
- await svc.install();
118
- }
119
- }
120
-
121
- if (plugin?.install) {
122
- await plugin.install();
123
- }
124
-
125
- core.logDiagnosticFiles(); // should this be moved into a separate install side-effect?
126
- } catch (err) {
127
- // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
128
- logger.error({ err }, ERROR_MESSAGE);
129
- }
130
-
131
- return runMain.apply(this, args);
132
- };
133
-
134
- return core;
135
- };
136
158
  } catch (err) {
137
159
  // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
138
160
  core.agentify = function agentify() {
@@ -151,3 +173,5 @@ module.exports = function init(core = {}) {
151
173
 
152
174
  return core.agentify;
153
175
  };
176
+
177
+ module.exports.DEFAULT_INSTALL_ORDER = DEFAULT_INSTALL_ORDER;
@@ -16,7 +16,7 @@
16
16
 
17
17
  'use strict';
18
18
 
19
- const Module = require('module');
19
+ const Module = require('node:module');
20
20
 
21
21
  /**
22
22
  * @param {import('.').Core & {
@@ -25,11 +25,12 @@ const Module = require('module');
25
25
  * @returns {import('@contrast/common').Installable}
26
26
  */
27
27
  module.exports = function init(core) {
28
+ const js = Module._extensions['.js'];
28
29
  const { _compile } = Module.prototype;
29
30
 
30
31
  core.rewriteHooks = {
31
32
  install() {
32
- if (!core.config.agent.node.enable_rewrite) return;
33
+ if (!core.config.agent.node.rewrite.enable) return;
33
34
 
34
35
  /**
35
36
  * @see https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js
@@ -37,35 +38,58 @@ module.exports = function init(core) {
37
38
  * @param {string} filename The file path of the module
38
39
  */
39
40
  Module.prototype._compile = function (content, filename) {
40
- let result;
41
+ /** @type {import('@contrast/rewriter').RewriteOpts} */
41
42
  const options = {
42
43
  filename,
43
44
  isModule: false,
44
45
  inject: true,
45
46
  wrap: true,
47
+ trim: false,
46
48
  };
47
- // if threadInfo is present, this is running with --loader or --import
48
- core.threadInfo?.post('rewrite', options);
49
49
 
50
- const { code } = core.rewriter.rewrite(content, options);
50
+ const result = core.rewriter.rewriteSync(content, options);
51
51
 
52
52
  try {
53
- result = _compile.call(this, code, filename);
53
+ const compiled = Reflect.apply(_compile, this, [result.code, filename]);
54
+
55
+ if (core.config.agent.node.rewrite.cache.enable) {
56
+ core.rewriter.cache.write(filename, result);
57
+ }
58
+
59
+ return compiled;
54
60
  } catch (err) {
55
61
  core.logger.warn(
56
62
  { err },
57
63
  'Failed to compile rewritten code for %s, compiling original code.',
58
64
  filename,
59
65
  );
60
- result = _compile.call(this, content, filename);
66
+
67
+ return Reflect.apply(_compile, this, [content, filename]);
61
68
  }
69
+ };
70
+
71
+ /**
72
+ * @see https://github.com/nodejs/node/blob/main/lib/internal/modules/cjs/loader.js
73
+ * @param {Module} module The module to compile
74
+ * @param {string} filename The file path of the module
75
+ */
76
+ Module._extensions['.js'] = function (module, filename) {
77
+ if (!core.config.agent.node.rewrite.cache.enable) {
78
+ return Reflect.apply(js, this, [module, filename]);
79
+ }
80
+
81
+ const cached = core.rewriter.cache.readSync(filename);
62
82
 
63
- return result;
83
+ // If cached, short circuit the _extensions method and go straight to compile.
84
+ return cached
85
+ ? Reflect.apply(_compile, module, [cached, filename])
86
+ : Reflect.apply(js, this, [module, filename]);
64
87
  };
65
88
  },
66
89
 
67
90
  uninstall() {
68
91
  Module.prototype._compile = _compile;
92
+ Module._extensions['.js'] = js;
69
93
  }
70
94
  };
71
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agentify",
3
- "version": "1.20.1",
3
+ "version": "1.22.0",
4
4
  "description": "Configures Contrast agent services and instrumentation within an application",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -17,18 +17,18 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/common": "1.18.0",
21
- "@contrast/config": "1.25.0",
22
- "@contrast/core": "1.29.1",
20
+ "@contrast/common": "1.19.0",
21
+ "@contrast/config": "1.26.0",
22
+ "@contrast/core": "1.30.0",
23
23
  "@contrast/deadzones": "1.1.2",
24
24
  "@contrast/dep-hooks": "1.3.1",
25
- "@contrast/esm-hooks": "2.2.1",
26
- "@contrast/instrumentation": "1.5.0",
27
- "@contrast/logger": "1.7.2",
28
- "@contrast/metrics": "1.4.0",
25
+ "@contrast/esm-hooks": "2.4.0",
26
+ "@contrast/instrumentation": "1.6.0",
27
+ "@contrast/logger": "1.8.0",
28
+ "@contrast/metrics": "1.6.0",
29
29
  "@contrast/patcher": "1.7.1",
30
- "@contrast/reporter": "1.24.0",
31
- "@contrast/rewriter": "1.4.2",
30
+ "@contrast/reporter": "1.25.1",
31
+ "@contrast/rewriter": "1.5.0",
32
32
  "@contrast/scopes": "1.4.0"
33
33
  }
34
34
  }
@@ -1,156 +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
- import Module from 'node:module';
17
- // want to only initialize some of the agent if main thread. not sure that's really
18
- // possible. based on logging, it looks like all of the low-level hooks (rewrite-injection,
19
- // assess-dataflow-propagators, assess-dataflow-sink ContrastMethods, and function-hooks)
20
- // are added in both the main thread and the loader thread. But all other modules are loaded
21
- // in the main thread. presumably, this is because the assess module was loaded in both
22
- // threads, registered the appropriate patchers in each, so both convert 'import' (statement
23
- // or function) to 'require'.
24
- //
25
- import { isMainThread, threadId } from 'node:worker_threads';
26
-
27
- const ERROR_MESSAGE = 'An error prevented the Contrast agent from installing. The application will be run without instrumentation.';
28
- const DEFAULT_INSTALL_ORDER = [
29
- 'reporter',
30
- 'contrastMethods',
31
- 'deadzones',
32
- 'scopes',
33
- 'sources',
34
- 'architectureComponents',
35
- 'assess',
36
- 'protect',
37
- 'depHooks',
38
- 'esmHooks',
39
- 'routeCoverage',
40
- 'libraryAnalysis',
41
- 'heapSnapshots',
42
- 'rewriteHooks',
43
- 'functionHooks',
44
- 'metrics',
45
- ];
46
-
47
- const require = Module.createRequire(import.meta.url);
48
- let startupError;
49
-
50
- /**
51
- * @param {object} { core = {}, options = {} }
52
- */
53
- async function loadModules({ core = {}, options = {} }) {
54
- try {
55
- require('@contrast/core/lib/messages')(core);
56
- require('@contrast/config')(core);
57
- if (core.config._errors?.length) {
58
- throw core.config._errors[0];
59
- }
60
- require('@contrast/logger').default(core);
61
-
62
- const thread = isMainThread ? 'main' : 'loader';
63
- core.logger.trace({ tid: threadId }, 'initializing core modules in %s thread', thread);
64
-
65
- if (isMainThread) {
66
- // @contrast/info ?
67
- require('@contrast/core/lib/agent-info')(core);
68
- require('@contrast/core/lib/system-info')(core);
69
- require('@contrast/core/lib/app-info')(core);
70
- if (core.appInfo._errors?.length) {
71
- throw core.appInfo._errors[0];
72
- }
73
- require('@contrast/core/lib/sensitive-data-masking')(core);
74
- require('@contrast/core/lib/is-agent-path')(core);
75
- require('@contrast/dep-hooks')(core);
76
- }
77
-
78
- const { default: install } = await import('@contrast/esm-hooks');
79
- const esmHooks = await install(core);
80
- core.esmHooks = esmHooks;
81
-
82
- if (isMainThread) {
83
- require('@contrast/patcher')(core);
84
- require('@contrast/core/lib/capture-stacktrace')(core);
85
- }
86
-
87
- require('@contrast/rewriter')(core); // merge contrast-methods?
88
-
89
- if (isMainThread) {
90
- require('@contrast/core/lib/contrast-methods')(core); // can we remove dependency on patcher?
91
-
92
- require('@contrast/scopes')(core);
93
- require('@contrast/deadzones')(core);
94
- require('@contrast/reporter').default(core);
95
- require('@contrast/instrumentation')(core);
96
- require('@contrast/metrics')(core);
97
-
98
- require('./heap-snapshots')(core);
99
- require('./sources')(core);
100
- require('./function-hooks')(core);
101
- require('./log-diagnostic-files')(core); // this doesn't really belong in agentify
102
- require('./rewrite-hooks')(core);
103
- }
104
-
105
- } catch (err) {
106
- // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
107
- if (core.logger) {
108
- core.logger.error({ err, threadId }, ERROR_MESSAGE);
109
- } else {
110
- console.error(new Error(ERROR_MESSAGE, { cause: err }));
111
- }
112
- startupError = err;
113
- }
114
- return core;
115
- }
116
-
117
- async function startAgent({ core, options = {} }) {
118
- if (startupError) return;
119
-
120
- const { executor, installOrder = DEFAULT_INSTALL_ORDER } = options;
121
- const { config, logger } = core;
122
-
123
- // this should be moved into config because errors are handled here now.
124
- for (const error of config._errors) {
125
- throw error;
126
- }
127
-
128
- logger.info({ tid: threadId }, 'Starting %s v%s', core.agentName, core.agentVersion);
129
- logger.info({ config }, 'Agent configuration');
130
-
131
- let plugin;
132
- if (typeof executor === 'function') {
133
- plugin = await executor(core);
134
- }
135
-
136
- for (const svcName of installOrder ?? []) {
137
- const svc = core[svcName];
138
- if (svc?.install) {
139
- logger.trace({ tid: threadId }, 'installing service: %s', svcName);
140
- await svc.install();
141
- }
142
- }
143
-
144
- if (plugin?.install) {
145
- await plugin.install();
146
- }
147
-
148
- // should this be moved into a separate install side-effect?
149
- if (isMainThread) {
150
- core.logDiagnosticFiles();
151
- }
152
-
153
- return core;
154
- }
155
-
156
- export { loadModules, startAgent };