@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 +53 -74
- package/lib/sensitive-data-masking/protect-listener.js +15 -15
- package/package.json +10 -9
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 {
|
|
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
|
|
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
|
|
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
|
|
57
|
+
version: config.application.version ?? pkgInfo.packageData.version,
|
|
59
58
|
serverVersion: config.server.version,
|
|
60
59
|
nodeVersion: process.version,
|
|
61
|
-
appPath: config.application.path
|
|
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
|
-
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
104
|
-
const args = [process.argv0, ...process.argv]
|
|
105
|
-
|
|
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 (
|
|
139
|
-
const main = path.join(
|
|
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
|
-
|
|
111
|
+
indexFile = main;
|
|
143
112
|
}
|
|
144
113
|
} catch (err) {
|
|
145
|
-
|
|
114
|
+
indexFile = null;
|
|
146
115
|
}
|
|
147
116
|
}
|
|
148
|
-
} catch (err) {
|
|
117
|
+
} catch (err) {
|
|
118
|
+
// use default process.argv[1]
|
|
119
|
+
}
|
|
149
120
|
|
|
150
|
-
if (!
|
|
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
|
|
157
|
-
if (
|
|
158
|
-
const message = 'application does not match exclusive_entrypoint config option';
|
|
159
|
-
logger.
|
|
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
|
|
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) {
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// packageData stays undefined, throws below.
|
|
184
|
+
}
|
|
200
185
|
}
|
|
201
186
|
|
|
202
187
|
if (!packageData) {
|
|
203
|
-
const message =
|
|
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
|
|
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, (
|
|
31
|
-
if (!
|
|
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
|
-
|
|
39
|
+
store.protect.parsedBody = `${CONTRAST_REDACTED}-body`;
|
|
40
40
|
} else {
|
|
41
|
-
traverseAndMask(
|
|
41
|
+
traverseAndMask(store.protect?.parsedBody, unmasked);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
traverseAndMask(
|
|
45
|
-
traverseAndMask(
|
|
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 =
|
|
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
|
-
|
|
55
|
-
|
|
54
|
+
store.sourceInfo.uriPath = StringPrototypeReplace.call(
|
|
55
|
+
store.sourceInfo.uriPath,
|
|
56
56
|
encoded,
|
|
57
57
|
redactedText
|
|
58
58
|
);
|
|
59
|
-
|
|
59
|
+
store.protect.parsedParams[key] = redactedText;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// raw headers
|
|
65
|
-
const 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 (
|
|
77
|
-
const searchParams = new URLSearchParams(
|
|
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
|
-
|
|
84
|
+
store.sourceInfo.queries = searchParams.toString();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (policy.maskAttackVector) {
|
|
88
88
|
// attack values
|
|
89
|
-
const inputAnalysis = Object.entries(
|
|
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.
|
|
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
|
+
"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.
|
|
23
|
-
"@contrast/config": "1.
|
|
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": "^
|
|
26
|
-
"@contrast/logger": "1.
|
|
27
|
-
"@contrast/patcher": "1.
|
|
28
|
-
"@contrast/perf": "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.
|
|
30
|
+
"axios": "^1.11.0",
|
|
31
|
+
"minimatch": "^9.0.5",
|
|
31
32
|
"semver": "^7.6.0"
|
|
32
33
|
}
|
|
33
34
|
}
|