@contrast/agentify 1.32.0 → 1.33.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.
@@ -18,6 +18,8 @@
18
18
  const fs = require('fs/promises');
19
19
  const path = require('path');
20
20
 
21
+ const { primordials: { JSONStringify } } = require('@contrast/common');
22
+
21
23
  module.exports = function init(core) {
22
24
  async function logDiagnosticFiles() {
23
25
  const { config, logger } = core;
@@ -32,13 +34,13 @@ module.exports = function init(core) {
32
34
  return Promise.all([
33
35
  fs.writeFile(
34
36
  path.join(report_path, 'contrast_effective_config.json'),
35
- JSON.stringify(effectiveConfig, null, 2)
37
+ JSONStringify(effectiveConfig, null, 2)
36
38
  ).catch((err) => {
37
39
  logger.warn({ err }, 'unable to write effective config file');
38
40
  }),
39
41
  fs.writeFile(
40
42
  path.join(report_path, 'contrast_system_info.json'),
41
- JSON.stringify(systemInfo, null, 2)
43
+ JSONStringify(systemInfo, null, 2)
42
44
  ).catch((err) => {
43
45
  logger.warn({ err }, 'unable to write system info file');
44
46
  }),
@@ -21,6 +21,8 @@ const IS_RETURN_REGEX = /^return\W/;
21
21
  const FUNCTION_DECL = 'function _contrast_';
22
22
  const VARIABLE_DECL = 'const _contrast_fn = ';
23
23
 
24
+ const { primordials: { StringPrototypeReplace, StringPrototypeSplit, RegExpPrototypeExec, FunctionPrototypeToString } } = require('@contrast/common');
25
+
24
26
  /**
25
27
  * Injects some context around a function's toString() to make it syntactically
26
28
  * valid. The `swc` parser expects input to be a valid `Statement` like one of
@@ -40,23 +42,23 @@ const VARIABLE_DECL = 'const _contrast_fn = ';
40
42
  */
41
43
  function contextualize(code) {
42
44
  // in case the class is anonymous we need to prepend a variable declaration.
43
- if (IS_CLASS_REGEX.exec(code)) {
45
+ if (RegExpPrototypeExec.call(IS_CLASS_REGEX, code)) {
44
46
  return `${VARIABLE_DECL}${code}`;
45
47
  }
46
48
 
47
49
  // if the function is a return, we don't need to add context.
48
- if (!IS_RETURN_REGEX.exec(code)) {
49
- const [orig] = code.split('{');
50
+ if (!RegExpPrototypeExec.call(IS_RETURN_REGEX, code)) {
51
+ const [orig] = StringPrototypeSplit.call(code, '{');
50
52
  let lineOne = orig;
51
53
 
52
54
  // When stringified, class methods look like normal function without the
53
55
  // `function` keyword. We can normalize this by prepending `function`.
54
- if (!IS_FUNCTION_REGEX.exec(lineOne) && lineOne.indexOf('=>') === -1) {
56
+ if (!RegExpPrototypeExec.call(IS_FUNCTION_REGEX, lineOne) && lineOne.indexOf('=>') === -1) {
55
57
  lineOne = `${FUNCTION_DECL}${lineOne}`;
56
58
  }
57
59
  lineOne = `${VARIABLE_DECL}${lineOne}`;
58
60
 
59
- code = code.replace(orig, lineOne);
61
+ code = StringPrototypeReplace.call(code, orig, lineOne);
60
62
  }
61
63
 
62
64
  return code;
@@ -70,8 +72,6 @@ module.exports = function (deps) {
70
72
  patcher: { hookedFunctions },
71
73
  } = deps;
72
74
 
73
- const toString = patcher.unwrap(Function.prototype.toString);
74
-
75
75
  const functionHooks = (deps.functionHooks = {
76
76
  installed: false,
77
77
  cache: new WeakMap(),
@@ -88,9 +88,9 @@ module.exports = function (deps) {
88
88
  try {
89
89
  let unwritten = contextualize(code);
90
90
  unwritten = rewriter.unwriteSync(unwritten);
91
- unwritten = unwritten.replace(FUNCTION_DECL, '');
92
- unwritten = unwritten.replace(VARIABLE_DECL, '');
93
- unwritten = unwritten.replace(/;\s*$/, ''); // removes trailing semicolon/whitespace
91
+ unwritten = StringPrototypeReplace.call(unwritten, FUNCTION_DECL, '');
92
+ unwritten = StringPrototypeReplace.call(unwritten, VARIABLE_DECL, '');
93
+ unwritten = StringPrototypeReplace.call(unwritten, /;\s*$/, ''); // removes trailing semicolon/whitespace
94
94
  return unwritten;
95
95
  } catch (err) {
96
96
  logger.warn({ err, code }, 'Failed to unwrite function code');
@@ -114,7 +114,7 @@ module.exports = function (deps) {
114
114
 
115
115
  let code;
116
116
  if (hookedFunctions.has(data.obj)) {
117
- code = toString.call(hookedFunctions.get(data.obj).fn);
117
+ code = FunctionPrototypeToString.call(hookedFunctions.get(data.obj).fn);
118
118
  } else {
119
119
  code = orig();
120
120
  }
@@ -135,7 +135,7 @@ module.exports = function (deps) {
135
135
  };
136
136
 
137
137
  functionHooks.uninstall = function () {
138
- Function.prototype.toString = toString;
138
+ Function.prototype.toString = FunctionPrototypeToString;
139
139
  };
140
140
 
141
141
  return functionHooks;
package/lib/index.d.ts CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { Installable } from '@contrast/common';
17
17
  import { Config } from '@contrast/config';
18
+ import { Perf } from '@contrast/perf';
18
19
  import { Core as _Core } from '@contrast/core';
19
20
  import { Deadzones } from '@contrast/deadzones';
20
21
  import { DepHooks } from '@contrast/dep-hooks';
@@ -53,6 +54,7 @@ declare module 'node:module' {
53
54
 
54
55
  export interface Core extends _Core {
55
56
  config: Config;
57
+ Perf: Perf;
56
58
  logger: Logger;
57
59
  depHooks: DepHooks;
58
60
  patcher: Patcher
package/lib/index.js CHANGED
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const Module = require('module');
19
+
19
20
  const {
20
21
  assertValidOpts,
21
22
  preStartupValidation,
@@ -53,7 +54,7 @@ const DEFAULT_INSTALL_ORDER = [
53
54
  ];
54
55
 
55
56
  /**
56
- * Utility for making agents: entrypoints indended to be run via --require, --loader, --import.
57
+ * Utility for making agents: entrypoints intended to be run via --require, --loader, --import.
57
58
  * Composes all needed dependencies during factory instantiation, and installs various components
58
59
  * that alter an application's environment in order for composed features to operate e.g. request scopes,
59
60
  * source code rewrite hooks etc.
@@ -62,9 +63,13 @@ const DEFAULT_INSTALL_ORDER = [
62
63
  */
63
64
  module.exports = function init(core = {}) {
64
65
  core.startTime = process.hrtime.bigint();
66
+ if (!core.Perf) {
67
+ core.Perf = require('@contrast/perf');
68
+ }
65
69
 
66
70
  let _callback;
67
71
  let _opts;
72
+ const _perf = new core.Perf('agentify');
68
73
 
69
74
  core.agentify = async function agentify(callback, opts = {}) {
70
75
  // options are hardcoded, so if this throws it's going to be in testing/dev situation
@@ -126,7 +131,10 @@ module.exports = function init(core = {}) {
126
131
  await plugin.install();
127
132
  }
128
133
  } catch (err) {
129
- // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
134
+ console.error(err);
135
+ console.error(ERROR_MESSAGE);
136
+
137
+ // TODO: Consider proper UNINSTALLATION and normal startup w/o agent
130
138
  logger.error({ err }, ERROR_MESSAGE);
131
139
  }
132
140
  }
@@ -194,7 +202,7 @@ module.exports = function init(core = {}) {
194
202
  if (isDefault) {
195
203
  mod = mod.default;
196
204
  }
197
- mod(core);
205
+ _perf.wrapInit(mod, spec)(core);
198
206
 
199
207
  // perform any validations that can take place now that this module is loaded.
200
208
  if (name === 'logger') {
@@ -215,11 +223,10 @@ module.exports = function init(core = {}) {
215
223
  // IntentionalError is used when the agent is disabled by config. We want
216
224
  // to abort the installation but not issue any messages.
217
225
  if (!(err instanceof IntentionalError)) {
226
+ console.error(err);
227
+ console.error(ERROR_MESSAGE);
218
228
  if (core.logger) {
219
- core.logger.error({ err }, ERROR_MESSAGE);
220
- } else {
221
- console.error(err);
222
- console.error(ERROR_MESSAGE);
229
+ core.logger?.error?.({ err }, ERROR_MESSAGE);
223
230
  }
224
231
  }
225
232
  }
package/lib/index.test.js CHANGED
@@ -4,10 +4,17 @@ const { expect } = require('chai');
4
4
  const proxyquire = require('proxyquire');
5
5
  const sinon = require('sinon');
6
6
  const mocks = require('@contrast/test/mocks');
7
+ const Perf = require('@contrast/perf');
7
8
 
8
9
  describe('agentify', function () {
9
10
  const Module = require('module');
10
11
  let agentify, preRunMain, runMain;
12
+ const allAgentify = new Map();
13
+ // the sinon stubs are captured in beforeEach(), before the agentify call,
14
+ // because the Perf wrapper will replace them on the core object. this way we
15
+ // can still determine that the function was called/not called as expected.
16
+ let loggerError;
17
+ let depHooksInstall;
11
18
 
12
19
  beforeEach(function () {
13
20
  // added because the config validation code verifies that the agent has
@@ -19,21 +26,33 @@ describe('agentify', function () {
19
26
  agentify = proxyquire('.', {
20
27
  '@contrast/logger': {
21
28
  default(core) {
22
- return core.logger = mocks.logger();
29
+ core.logger = mocks.logger();
30
+ loggerError = core.logger.error;
31
+ return core.logger;
23
32
  }
24
33
  },
25
34
  '@contrast/dep-hooks'(core) {
26
- return core.depHooks = mocks.depHooks();
35
+ core.depHooks = mocks.depHooks();
36
+ depHooksInstall = core.depHooks.install;
37
+ return core.depHooks;
27
38
  },
28
39
  })();
29
40
  preRunMain = sinon.stub();
30
41
  runMain = sinon.stub(Module, 'runMain');
31
42
  });
32
43
 
44
+ afterEach(function() {
45
+ Perf.fromAllToMap('agentify', allAgentify);
46
+ });
47
+
33
48
  after(function() {
34
49
  delete process.env.CONTRAST__API__API_KEY;
35
50
  delete process.env.CONTRAST__API__SERVICE_KEY;
36
51
  delete process.env.CONTRAST__API_USER_NAME;
52
+ const stats = Perf.getStats(allAgentify);
53
+ for (const [key, { n, totalMicros, mean }] of stats.entries()) {
54
+ console.log(key, n, totalMicros, 'nsec', mean, 'mean');
55
+ }
37
56
  });
38
57
 
39
58
  it('invoking with callback will initialize and patch runMain', async function () {
@@ -46,16 +65,16 @@ describe('agentify', function () {
46
65
 
47
66
  expect(preRunMain).to.have.been.calledWith(core);
48
67
  expect(runMain).to.have.been.called;
49
- expect(core.depHooks.install).to.have.been.called;
50
- expect(core.logger.error).not.to.have.been.called;
68
+ expect(depHooksInstall).to.have.been.called;
69
+ expect(loggerError).not.to.have.been.called;
51
70
  });
52
71
 
53
72
  it('will not run install methods when installOrder option is empty array', async function () {
54
- const core = await agentify(preRunMain, { installOrder: [] });
73
+ const _core = await agentify(preRunMain, { installOrder: [] });
55
74
  await Module.runMain();
56
75
 
57
- expect(core.depHooks.install).not.to.have.been.called;
58
- expect(core.logger.error).not.to.have.been.called;
76
+ expect(depHooksInstall).not.called;
77
+ expect(loggerError).not.called;
59
78
  });
60
79
 
61
80
 
@@ -121,7 +140,7 @@ describe('agentify', function () {
121
140
 
122
141
  await Module.runMain();
123
142
 
124
- expect(core.depHooks.install).not.to.have.been.called;
143
+ expect(depHooksInstall).not.to.have.been.called;
125
144
  expect(runMain).to.have.been.called;
126
145
  expect(core.logger.error).to.have.been.calledWith({ err });
127
146
  });
@@ -37,9 +37,10 @@ const DEADZONED_PATHS = [
37
37
  'browserify',
38
38
  'bson',
39
39
  'bunyan',
40
- '@cyclonedx',
40
+ 'chai', // not sure why chai was rewritten
41
41
  'coffeescript',
42
42
  'compression',
43
+ '@cyclonedx',
43
44
  'etag',
44
45
  // 'cookie', // todo: verify this doesn't break sources/propagation (*)
45
46
  // 'cookie-signature', // (*)
package/lib/utils.js CHANGED
@@ -25,6 +25,8 @@ const {
25
25
  }
26
26
  } = require('../package.json');
27
27
 
28
+ const { primordials: { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeTrim } } = require('@contrast/common');
29
+
28
30
  /**
29
31
  * Performs various startup validation checks. All checks throw errors when they
30
32
  * fail so startup/installation stops.
@@ -45,10 +47,9 @@ function preStartupValidation(core) {
45
47
  function assertSupportedNodeVersion(engines) {
46
48
  if (!semver.satisfies(process.version, engines)) {
47
49
  let validRanges = '';
48
- engines.split('||').forEach((range) => {
50
+ StringPrototypeSplit.call(engines, '||').forEach((range) => {
49
51
  const minVersion = semver.minVersion(range).toString();
50
- const maxVersion = range.split('<').pop()
51
- .trim();
52
+ const maxVersion = StringPrototypeTrim.call(StringPrototypeSplit.call(range, '<').pop());
52
53
  validRanges += `${minVersion} and ${maxVersion}, `;
53
54
  });
54
55
  throw new Error(`Contrast only officially supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
@@ -72,8 +73,8 @@ function assertSupportedPreloadUsage() {
72
73
  if (CSI_EXPOSE_CORE === 'no-validate') return;
73
74
 
74
75
  // eslint-disable-next-line newline-per-chained-call
75
- const [major, minor] = version.slice(1).split('.').map(Number);
76
- const nodeOptionArgs = (NODE_OPTIONS || '').split(/\s+/).filter((v) => v);
76
+ const [major, minor] = StringPrototypeSplit.call(StringPrototypeSlice.call(version, 1), '.').map(Number);
77
+ const nodeOptionArgs = StringPrototypeSplit.call(NODE_OPTIONS || '', /\s+/).filter((v) => v);
77
78
  const allArgsToCheck = [...execArgv, ...nodeOptionArgs];
78
79
 
79
80
  let checkOccurred = false;
package/lib/validators.js CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { IntentionalError } = require('@contrast/common');
18
+ const { IntentionalError, primordials: { ArrayPrototypeSlice, StringPrototypeToLowerCase } } = require('@contrast/common');
19
19
 
20
20
  // v4 accepted `-c` or `--configFile` option in argv, but v5 does not. so if
21
21
  // something that looks like a config flag is present on the command line, we
@@ -49,8 +49,8 @@ function getConfigFlag(argv) {
49
49
  // let's not because that seems false-negative prone.
50
50
 
51
51
  // if the next arg is another flag, then this isn't a contrast config flag
52
- if (!argv[i + 1].startsWith('-') && argv[i + 1].toLowerCase().includes('contrast')) {
53
- return argv.slice(i, i + 2);
52
+ if (!argv[i + 1].startsWith('-') && StringPrototypeToLowerCase.call(argv[i + 1]).includes('contrast')) {
53
+ return ArrayPrototypeSlice.call(argv, i, i + 2);
54
54
  }
55
55
  }
56
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agentify",
3
- "version": "1.32.0",
3
+ "version": "1.33.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,20 +17,21 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/common": "1.25.0",
21
- "@contrast/config": "1.33.0",
22
- "@contrast/core": "1.37.0",
23
- "@contrast/deadzones": "1.7.0",
24
- "@contrast/dep-hooks": "1.5.0",
25
- "@contrast/esm-hooks": "2.11.0",
20
+ "@contrast/common": "1.26.0",
21
+ "@contrast/config": "1.34.0",
22
+ "@contrast/core": "1.38.0",
23
+ "@contrast/deadzones": "1.8.0",
24
+ "@contrast/dep-hooks": "1.6.0",
25
+ "@contrast/esm-hooks": "2.12.0",
26
26
  "@contrast/find-package-json": "^1.1.0",
27
- "@contrast/instrumentation": "1.15.0",
28
- "@contrast/logger": "1.10.0",
29
- "@contrast/metrics": "1.13.0",
30
- "@contrast/patcher": "1.9.0",
31
- "@contrast/reporter": "1.32.0",
32
- "@contrast/rewriter": "1.13.0",
33
- "@contrast/scopes": "1.6.0",
27
+ "@contrast/instrumentation": "1.16.0",
28
+ "@contrast/logger": "1.11.0",
29
+ "@contrast/metrics": "1.14.0",
30
+ "@contrast/patcher": "1.10.0",
31
+ "@contrast/perf": "1.1.0",
32
+ "@contrast/reporter": "1.33.0",
33
+ "@contrast/rewriter": "1.14.0",
34
+ "@contrast/scopes": "1.7.0",
34
35
  "semver": "^7.6.0"
35
36
  }
36
37
  }