@contrast/core 1.54.2 → 1.56.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/app-info.js CHANGED
@@ -20,7 +20,8 @@ const fs = require('fs');
20
20
  const path = require('path');
21
21
  const semver = require('semver');
22
22
  const process = require('process');
23
- const { IntentionalError, primordials: { ArrayPrototypeJoin } } = require('@contrast/common');
23
+ const { minimatch } = require('minimatch');
24
+ const { IntentionalError } = require('@contrast/common');
24
25
  const { findPackageJsonSync } = require('@contrast/find-package-json');
25
26
 
26
27
  /**
@@ -35,10 +36,8 @@ module.exports = function (core) {
35
36
  const { app_root, cmd_ignore_list, exclusive_entrypoint } = config.agent.node;
36
37
 
37
38
  checkPreLoadFlag();
38
- const cmd = getCommand();
39
39
  const pkgInfo = getPackageInfo();
40
- const entrypoint = getEntrypoint();
41
- const name = getApplicationName();
40
+ const { cmd, indexFile } = parseArgv();
42
41
 
43
42
  core.appInfo = {
44
43
  // dedupe this? - it's already in systemInfo
@@ -50,15 +49,15 @@ module.exports = function (core) {
50
49
  },
51
50
  cmd,
52
51
  hostname: os.hostname(),
53
- indexFile: entrypoint,
52
+ indexFile,
54
53
  path: pkgInfo.packageFile,
55
54
  pkg: pkgInfo.packageData,
56
- name,
55
+ name: getApplicationName(),
57
56
  app_dir: pkgInfo.dir,
58
- version: config.application.version || pkgInfo.packageData.version,
57
+ version: config.application.version ?? pkgInfo.packageData.version,
59
58
  serverVersion: config.server.version,
60
59
  nodeVersion: process.version,
61
- appPath: config.application.path || pkgInfo.dir,
60
+ appPath: config.application.path ?? pkgInfo.dir,
62
61
  serverName: config.server.name,
63
62
  serverType: config.server.type,
64
63
  serverEnvironment: config.server.environment,
@@ -76,13 +75,12 @@ module.exports = function (core) {
76
75
  } = process;
77
76
  [
78
77
  { range: '>=18.19.0', flags: ['--import'] },
79
- { range: '>=16.17.0 <18.19.0', flags: ['--loader'] },
80
- { range: '<16.17.0', flags: ['-r', '--require'] }
78
+ { range: '>=16.17.0 <18.19.0', flags: ['--loader'] }
81
79
  ].forEach(({ range, flags }) => {
82
80
  if (
83
81
  semver.satisfies(version, range) &&
84
82
  (execArgv.some((el, idx) => el === '@contrast/agent' && !flags.includes(execArgv[idx - 1])) ||
85
- NODE_OPTIONS?.includes('@contrast/agent') && !flags.some(flag => NODE_OPTIONS.includes(flag)))
83
+ NODE_OPTIONS?.includes('@contrast/agent') && !flags.some(flag => NODE_OPTIONS.includes(flag)))
86
84
  ) {
87
85
  logger.warn(
88
86
  'For Node LTS %s, use %s command to run the agent. See: https://docs.contrastsecurity.com/en/install-node-js.html',
@@ -94,84 +92,69 @@ module.exports = function (core) {
94
92
  }
95
93
 
96
94
  /**
97
- * Generates a command string based on ARGV and process.argv0, which will be used
98
- * for the `appInfo.cmd` field. This function will throw if any of the config's
99
- * cmd_ignore_list values match the command.
100
- * @returns {string} the issued command that started the current node process
95
+ * Parse and process the process.argv array to construct `appInfo.cmd` and
96
+ * resolve the application entry point.
97
+ * Throws if the running process should not be instrumented per the
98
+ * `cmd_ignore_list` and `exclusive_entrypoint` config options.
101
99
  * @throws {IntentionalError} when command should be is ignored by Contrast
100
+ * @returns {{ cmd: string, indexFile: string }} stringified command and resolved entrypoint file
102
101
  */
103
- function getCommand() {
104
- const args = [process.argv0, ...process.argv].map((a) => path.basename(a));
105
- const cmd = ArrayPrototypeJoin.call(Array.from(new Set(args)), ' ');
106
- const message = 'application command matches cmd_ignore_list config option';
107
-
108
- if (cmd_ignore_list) {
109
- let err;
110
- for (const ignoreCommand of cmd_ignore_list) {
111
-
112
- if (ignoreCommand === 'npm*') {
113
- if (cmd.includes('npm ')) err = new IntentionalError(message);
114
- } else {
115
- if (cmd.includes(ignoreCommand)) err = new IntentionalError(message);
116
- }
117
- if (err) {
118
- logger.trace({ cmd_ignore_list, cmd }, message);
119
- throw err;
120
- }
121
- }
122
- }
123
-
124
- return cmd;
125
- }
126
-
127
- /**
128
- * Returns the entrypoint file. If none is found, or the one discovered doesn't match the
129
- * config's `agent.node.exclusive_entrypoint` value, this will throw.
130
- * @returns {string} entrypoint file name
131
- * @throws {Error|IntentionalError} if no entrypoint is found or we're supposed to ignore the app
132
- */
133
- function getEntrypoint() {
134
- let entrypoint = process.argv[1];
135
- const { packageData } = pkgInfo;
102
+ function parseArgv() {
103
+ const args = new Set([process.argv0, ...process.argv]);
104
+ let indexFile = path.normalize(process.argv[1]);
136
105
 
137
106
  try {
138
- if (entrypoint && fs.statSync(entrypoint).isDirectory()) {
139
- const main = path.join(entrypoint, packageData.main || 'index.js');
107
+ if (indexFile && fs.statSync(indexFile).isDirectory()) {
108
+ const main = path.join(indexFile, pkgInfo.packageData.main ?? 'index.js');
140
109
  try {
141
110
  if (fs.statSync(main)) {
142
- entrypoint = main;
111
+ indexFile = main;
143
112
  }
144
113
  } catch (err) {
145
- entrypoint = null;
114
+ indexFile = null;
146
115
  }
147
116
  }
148
- } catch (err) {} // eslint-disable-line no-empty
117
+ } catch (err) {
118
+ // use default process.argv[1]
119
+ }
149
120
 
150
- if (!entrypoint) {
121
+ if (!indexFile) {
151
122
  logger.error('no entrypoint found for application');
152
123
  throw new Error('No entrypoint found');
153
124
  }
154
125
 
126
+ let isExclusiveEntrypoint = false;
155
127
  if (exclusive_entrypoint) {
156
- const expectedEntrypoint = path.resolve(app_root || process.cwd(), exclusive_entrypoint);
157
- if (entrypoint !== expectedEntrypoint) {
158
- const message = 'application does not match exclusive_entrypoint config option';
159
- logger.trace({
160
- entrypoint,
161
- exclusive_entrypoint: expectedEntrypoint,
162
- }, message);
128
+ const expectedEntrypoint = path.resolve(app_root ?? process.cwd(), exclusive_entrypoint);
129
+ if (indexFile !== expectedEntrypoint) {
130
+ const message = 'Skipping instrumentation because application does not match the `exclusive_entrypoint` config option.';
131
+ logger.info({ entrypoint: indexFile, exclusive_entrypoint: expectedEntrypoint }, message);
163
132
  throw new IntentionalError(message);
164
133
  }
134
+ isExclusiveEntrypoint = true;
135
+ }
136
+
137
+ const cmd = process.argv.map((arg) => path.basename(arg)).join(' ');
138
+
139
+ if (!isExclusiveEntrypoint) {
140
+ for (const arg of args) {
141
+ for (const pattern of cmd_ignore_list) {
142
+ if (minimatch(arg, pattern, { dot: true })) {
143
+ const message = 'Skipping instrumentation because application command matches the `cmd_ignore_list` config option.';
144
+ logger.info({ cmd_ignore_list, cmd, arg, pattern }, message);
145
+ throw new IntentionalError(message);
146
+ }
147
+ }
148
+ }
165
149
  }
166
150
 
167
- return entrypoint;
151
+ return { cmd, indexFile };
168
152
  }
169
153
 
170
154
  /**
171
155
  * Will try to read the `package.json` file of the app. This will use find-pacakge-json
172
156
  * starting first from entrypoint, then from CWD.
173
157
  * NOTE: If the `app_root` value is specified, this will check only there and then throw if not found.
174
- * @param {string} entrypoint app entrypoint
175
158
  * @returns {PackageInfo} dir, packageData, and packageFile
176
159
  * @throws {Error} if package can't be found or parsed
177
160
  */
@@ -196,15 +179,14 @@ module.exports = function (core) {
196
179
  packageFile = process.env.npm_package_json ?? findPackageJsonSync({ cwd: dir });
197
180
  packageData = require(packageFile);
198
181
  break;
199
- } catch (err) {} // eslint-disable-line no-empty
182
+ } catch (err) {
183
+ // packageData stays undefined, throws below.
184
+ }
200
185
  }
201
186
 
202
187
  if (!packageData) {
203
- const message = 'unable to locate application package.json';
204
- logger.error({
205
- app_root,
206
- paths: Array.from(dirs),
207
- }, message);
188
+ const message = "Unable to locate application's package.json";
189
+ logger.error({ app_root, paths: Array.from(dirs) }, message);
208
190
  throw new Error(message);
209
191
  }
210
192
 
@@ -220,12 +202,9 @@ module.exports = function (core) {
220
202
  * @throws {Error} if there is no name identified
221
203
  */
222
204
  function getApplicationName() {
223
- const name = config.application.name || pkgInfo.packageData.name;
205
+ const name = config.application.name ?? pkgInfo.packageData.name;
224
206
  if (!name) {
225
- throw new Error(
226
- 'The application\'s name was not identified. ' +
227
- 'Please provide name in package.json field or with the agent\'s application.name config option.'
228
- );
207
+ throw new Error("The application's name was not identified. Please provide `name` in package.json field or with the agent's `application.name` config option.");
229
208
  }
230
209
 
231
210
  return name;
@@ -27,8 +27,8 @@ module.exports = function (core) {
27
27
  sensitiveDataMasking: { policy, getRedactedText, traverseAndMask },
28
28
  } = core;
29
29
 
30
- messages.on(Event.PROTECT, (msg) => {
31
- if (!msg.protect || !policy.keywordSets.length) {
30
+ messages.on(Event.PROTECT, (store) => {
31
+ if (!store.protect || !policy.keywordSets.length || !store.sourceInfo) {
32
32
  return;
33
33
  }
34
34
 
@@ -36,33 +36,33 @@ module.exports = function (core) {
36
36
 
37
37
  const unmasked = policy.maskAttackVector ? new Set() : undefined;
38
38
  if (policy.maskHttpBody) {
39
- msg.protect.parsedBody = `${CONTRAST_REDACTED}-body`;
39
+ store.protect.parsedBody = `${CONTRAST_REDACTED}-body`;
40
40
  } else {
41
- traverseAndMask(msg.protect?.parsedBody, unmasked);
41
+ traverseAndMask(store.protect?.parsedBody, unmasked);
42
42
  }
43
43
 
44
- traverseAndMask(msg.protect?.parsedCookies, unmasked);
45
- traverseAndMask(msg.protect?.parsedQuery, unmasked);
44
+ traverseAndMask(store.protect?.parsedCookies, unmasked);
45
+ traverseAndMask(store.protect?.parsedQuery, unmasked);
46
46
 
47
47
  // Do parsed URL path params and urlPath together
48
- const params = msg.protect?.parsedParams;
48
+ const params = store.protect?.parsedParams;
49
49
  if (params) {
50
50
  for (const [key, value] of Object.entries(params)) {
51
51
  const redactedText = getRedactedText(key);
52
52
  if (redactedText) {
53
53
  const encoded = encodeURIComponent(value);
54
- msg.protect.reqData.uriPath = StringPrototypeReplace.call(
55
- msg.protect.reqData.uriPath,
54
+ store.sourceInfo.uriPath = StringPrototypeReplace.call(
55
+ store.sourceInfo.uriPath,
56
56
  encoded,
57
57
  redactedText
58
58
  );
59
- msg.protect.parsedParams[key] = redactedText;
59
+ store.protect.parsedParams[key] = redactedText;
60
60
  }
61
61
  }
62
62
  }
63
63
 
64
64
  // raw headers
65
- const headers = msg.protect?.reqData.headers;
65
+ const headers = store.sourceInfo.rawHeaders;
66
66
  for (let i = 0; i <= headers.length - 2; i += 2) {
67
67
  const key = headers[i];
68
68
 
@@ -73,20 +73,20 @@ module.exports = function (core) {
73
73
  }
74
74
 
75
75
  // raw queries
76
- if (msg.protect?.reqData?.queries) {
77
- const searchParams = new URLSearchParams(msg.protect.reqData.queries);
76
+ if (store.sourceInfo?.queries) {
77
+ const searchParams = new URLSearchParams(store.sourceInfo.queries);
78
78
  for (const [key] of searchParams) {
79
79
  const redactedText = getRedactedText(key);
80
80
  if (redactedText) {
81
81
  searchParams.set(key, redactedText);
82
82
  }
83
83
  }
84
- msg.protect.reqData.queries = searchParams.toString();
84
+ store.sourceInfo.queries = searchParams.toString();
85
85
  }
86
86
 
87
87
  if (policy.maskAttackVector) {
88
88
  // attack values
89
- const inputAnalysis = Object.entries(msg.protect?.resultsMap);
89
+ const inputAnalysis = Object.entries(store.protect?.resultsMap);
90
90
  for (const [, results] of inputAnalysis) {
91
91
  for (const result of results) {
92
92
  const redactedText = getRedactedText(result.key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/core",
3
- "version": "1.54.2",
3
+ "version": "1.56.0",
4
4
  "description": "Preconfigured Contrast agent core services and models",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -13,21 +13,22 @@
13
13
  "types": "lib/index.d.ts",
14
14
  "engines": {
15
15
  "npm": ">=6.13.7 <7 || >= 8.3.1",
16
- "node": ">= 16.9.1"
16
+ "node": ">= 18.7.0"
17
17
  },
18
18
  "scripts": {
19
19
  "test": "bash ../scripts/test.sh"
20
20
  },
21
21
  "dependencies": {
22
- "@contrast/common": "1.34.2",
23
- "@contrast/config": "1.49.2",
22
+ "@contrast/common": "1.36.0",
23
+ "@contrast/config": "1.51.0",
24
24
  "@contrast/find-package-json": "^1.1.0",
25
- "@contrast/fn-inspect": "^4.3.0",
26
- "@contrast/logger": "1.27.2",
27
- "@contrast/patcher": "1.26.2",
28
- "@contrast/perf": "1.3.1",
25
+ "@contrast/fn-inspect": "^5.0.2",
26
+ "@contrast/logger": "1.29.0",
27
+ "@contrast/patcher": "1.28.0",
28
+ "@contrast/perf": "1.4.0",
29
29
  "@tsxper/crc32": "^2.1.3",
30
- "axios": "^1.7.4",
30
+ "axios": "^1.11.0",
31
+ "minimatch": "^9.0.5",
31
32
  "semver": "^7.6.0"
32
33
  }
33
34
  }