@contrast/agentify 1.20.0 → 1.21.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/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,87 @@ 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
+ let _callback;
59
+ let _opts;
60
+
61
+ core.agentify = async function agentify(callback, opts = {}) {
62
+ _callback = callback;
63
+ _opts = opts;
64
+ _opts.type = _opts.type ?? 'cjs';
65
+ _opts.installOrder = _opts.installOrder ?? DEFAULT_INSTALL_ORDER;
66
+
67
+ // for 'cjs' we hook runMain and install prior to running it
68
+ if (_opts.type === 'cjs') {
69
+ if (typeof callback !== 'function') {
70
+ throw new Error('Invalid usage: first argument must be a function');
71
+ }
72
+ const { runMain } = Module;
73
+ /**
74
+ * This is one side effect that will always occur, even with `installOrder: []`.
75
+ * The act of calling `agentify()` is enough to cause this side effect, by design.
76
+ */
77
+ Module.runMain = async function (...args) {
78
+ await install();
79
+ return runMain.apply(this, args);
80
+ };
81
+ } else {
82
+ // for 'esm' we load esm-hooks support, which will install lastly
83
+ const { default: esmHooks } = await import('@contrast/esm-hooks/lib/index.mjs');
84
+ esmHooks(core);
85
+
86
+ await install();
87
+ }
88
+
89
+ return core;
90
+ };
91
+
92
+ async function install() {
93
+ const { config, logger } = core;
94
+
95
+ try {
96
+ for (const err of config._errors) {
97
+ throw err; // move this into config itself since we now handle errors
98
+ }
99
+
100
+ logger.info('Starting %s v%s', core.agentName, core.agentVersion);
101
+ logger.info({ config }, 'Agent configuration');
102
+
103
+ const plugin = await _callback?.(core);
104
+
105
+ for (const svcName of _opts.installOrder ?? []) {
106
+ const svc = core[svcName];
107
+ if (svc?.install) {
108
+ logger.trace('installing service: %s', svcName);
109
+ await svc.install();
110
+ }
111
+ }
112
+
113
+ if (plugin?.install) {
114
+ await plugin.install();
115
+ }
116
+
117
+ core.logDiagnosticFiles(); // should this be moved into a separate install side-effect?
118
+ } catch (err) {
119
+ // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
120
+ logger.error({ err }, ERROR_MESSAGE);
121
+ }
122
+ }
123
+
45
124
  try {
46
125
  require('@contrast/core/lib/messages')(core);
47
126
  require('@contrast/config')(core);
@@ -49,8 +128,6 @@ module.exports = function init(core = {}) {
49
128
  throw core.config._errors[0];
50
129
  }
51
130
  require('@contrast/logger').default(core);
52
-
53
- // @contrast/info ?
54
131
  require('@contrast/core/lib/agent-info')(core);
55
132
  require('@contrast/core/lib/system-info')(core);
56
133
  require('@contrast/core/lib/app-info')(core);
@@ -62,77 +139,20 @@ module.exports = function init(core = {}) {
62
139
  require('@contrast/dep-hooks')(core);
63
140
  require('@contrast/patcher')(core);
64
141
  require('@contrast/core/lib/capture-stacktrace')(core);
65
-
66
142
  require('@contrast/rewriter')(core); // merge contrast-methods?
67
143
  require('@contrast/core/lib/contrast-methods')(core); // can we remove dependency on patcher?
68
-
69
144
  require('@contrast/scopes')(core);
70
145
  require('@contrast/deadzones')(core);
71
146
  require('@contrast/reporter').default(core);
72
147
  require('@contrast/instrumentation')(core);
73
148
  require('@contrast/metrics')(core);
74
149
 
75
- // compose add'l local services
150
+ // compose additional local services
76
151
  require('./heap-snapshots')(core);
77
152
  require('./sources')(core);
78
153
  require('./function-hooks')(core);
79
154
  require('./log-diagnostic-files')(core); // this doesn't really belong in agentify
80
155
  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
156
  } catch (err) {
137
157
  // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
138
158
  core.agentify = function agentify() {
@@ -151,3 +171,5 @@ module.exports = function init(core = {}) {
151
171
 
152
172
  return core.agentify;
153
173
  };
174
+
175
+ module.exports.DEFAULT_INSTALL_ORDER = DEFAULT_INSTALL_ORDER;
@@ -44,8 +44,6 @@ module.exports = function init(core) {
44
44
  inject: true,
45
45
  wrap: true,
46
46
  };
47
- // if threadInfo is present, this is running with --loader or --import
48
- core.threadInfo?.post('rewrite', options);
49
47
 
50
48
  const { code } = core.rewriter.rewrite(content, options);
51
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agentify",
3
- "version": "1.20.0",
3
+ "version": "1.21.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,17 +17,17 @@
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.0",
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.0",
26
- "@contrast/instrumentation": "1.5.0",
27
- "@contrast/logger": "1.7.2",
28
- "@contrast/metrics": "1.4.0",
25
+ "@contrast/esm-hooks": "2.3.0",
26
+ "@contrast/instrumentation": "1.6.0",
27
+ "@contrast/logger": "1.8.0",
28
+ "@contrast/metrics": "1.5.0",
29
29
  "@contrast/patcher": "1.7.1",
30
- "@contrast/reporter": "1.24.0",
30
+ "@contrast/reporter": "1.25.0",
31
31
  "@contrast/rewriter": "1.4.2",
32
32
  "@contrast/scopes": "1.4.0"
33
33
  }
@@ -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 };