@contrast/agent 4.20.2 → 4.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.
- package/bin/VERSION +1 -1
- package/bin/contrast-service-darwin-arm64 +0 -0
- package/bin/{mac/contrast-service → contrast-service-darwin-x64} +0 -0
- package/bin/contrast-service-linux-arm64 +0 -0
- package/bin/{linux/contrast-service → contrast-service-linux-x64} +0 -0
- package/bin/{windows/contrast-service.exe → contrast-service-win32-x64} +0 -0
- package/lib/assess/hapi/sources.js +1 -1
- package/lib/assess/policy/signatures.json +12 -0
- package/lib/assess/propagators/validator/init-hooks.js +44 -5
- package/lib/assess/propagators/validator/validator-methods.js +6 -0
- package/lib/core/hapi/index.js +15 -2
- package/lib/reporter/speedracer/index.js +44 -70
- package/lib/util/heap-dump.js +41 -29
- package/package.json +5 -6
package/bin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.28.
|
|
1
|
+
2.28.22
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -28,7 +28,7 @@ class HapiAssessSources {
|
|
|
28
28
|
* Handles emitting of all assess source events
|
|
29
29
|
*/
|
|
30
30
|
registerSourcesHandler() {
|
|
31
|
-
agentEmitter.on(EVENTS.
|
|
31
|
+
agentEmitter.on(EVENTS.HAPI_POST_AUTH, ({ request }) => {
|
|
32
32
|
emitWatchableObjects(request);
|
|
33
33
|
});
|
|
34
34
|
}
|
|
@@ -1000,6 +1000,12 @@
|
|
|
1000
1000
|
"methodName": "isCreditCard",
|
|
1001
1001
|
"isModule": true
|
|
1002
1002
|
},
|
|
1003
|
+
"validator.isDate": {
|
|
1004
|
+
"moduleName": "validator",
|
|
1005
|
+
"version": ">=13.0.0",
|
|
1006
|
+
"methodName": "isDecimal",
|
|
1007
|
+
"isModule": true
|
|
1008
|
+
},
|
|
1003
1009
|
"validator.isDecimal": {
|
|
1004
1010
|
"moduleName": "validator",
|
|
1005
1011
|
"version": ">=13.0.0",
|
|
@@ -1018,6 +1024,12 @@
|
|
|
1018
1024
|
"methodName": "isEAN",
|
|
1019
1025
|
"isModule": true
|
|
1020
1026
|
},
|
|
1027
|
+
"validator.isEmail": {
|
|
1028
|
+
"moduleName": "validator",
|
|
1029
|
+
"version": ">=13.0.0",
|
|
1030
|
+
"methodName": "isEmail",
|
|
1031
|
+
"isModule": true
|
|
1032
|
+
},
|
|
1021
1033
|
"validator.isEthereumAddress": {
|
|
1022
1034
|
"moduleName": "validator",
|
|
1023
1035
|
"version": ">=13.0.0",
|
|
@@ -39,11 +39,11 @@ const agent = require('../../../agent');
|
|
|
39
39
|
* https://docs.google.com/spreadsheets/d/17p5C6NOISNuWi8D-07d8gNg8byQytuwjtgBgzGpfU_w/edit#gid=0
|
|
40
40
|
*
|
|
41
41
|
*/
|
|
42
|
-
module.exports.handle = function() {
|
|
42
|
+
module.exports.handle = function () {
|
|
43
43
|
const {
|
|
44
44
|
validators,
|
|
45
45
|
untrackers,
|
|
46
|
-
sanitizers
|
|
46
|
+
sanitizers,
|
|
47
47
|
} = require('./validator-methods.js');
|
|
48
48
|
|
|
49
49
|
const patchType = PATCH_TYPES.ASSESS_PROPAGATOR;
|
|
@@ -62,7 +62,7 @@ module.exports.handle = function() {
|
|
|
62
62
|
const patched = patcher.patch(obj, {
|
|
63
63
|
name,
|
|
64
64
|
patchType,
|
|
65
|
-
post
|
|
65
|
+
post,
|
|
66
66
|
});
|
|
67
67
|
// babel adds a self-referential default property so if present in the unpatched
|
|
68
68
|
// object update the patched object
|
|
@@ -89,7 +89,12 @@ module.exports.handle = function() {
|
|
|
89
89
|
{ name: 'validator', file: `lib/${validator}` },
|
|
90
90
|
(index, meta) => {
|
|
91
91
|
function post(data) {
|
|
92
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
data.result &&
|
|
94
|
+
(validator !== 'matches' ||
|
|
95
|
+
(validator === 'matches' &&
|
|
96
|
+
agent.config.assess.trust_custom_validators))
|
|
97
|
+
) {
|
|
93
98
|
const trackingData = tracker.getData(data.args[0]);
|
|
94
99
|
if (trackingData) {
|
|
95
100
|
tagRangeUtil.addInPlace(
|
|
@@ -104,7 +109,7 @@ module.exports.handle = function() {
|
|
|
104
109
|
signature,
|
|
105
110
|
tagRanges: trackingData.tagRanges,
|
|
106
111
|
source: 'O',
|
|
107
|
-
target: 'R'
|
|
112
|
+
target: 'R',
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
event.parents.push(trackingData.event);
|
|
@@ -210,4 +215,38 @@ module.exports.handle = function() {
|
|
|
210
215
|
}
|
|
211
216
|
);
|
|
212
217
|
}
|
|
218
|
+
|
|
219
|
+
moduleHook.resolve({ name: 'validator', file: 'lib/isEmail' }, (isEmail) => {
|
|
220
|
+
const signature = new Signature('validator.isEmail');
|
|
221
|
+
|
|
222
|
+
function post(data) {
|
|
223
|
+
const trackingData = tracker.getData(data.args[0]);
|
|
224
|
+
// The default options for the two fields of interest are:
|
|
225
|
+
// `{ allow_display_name: false, require_display_name: false }`
|
|
226
|
+
// so we can use an empty object as it will also yield
|
|
227
|
+
// falsy values for these two fields
|
|
228
|
+
const options = data.args[1] ? data.args[1] : {};
|
|
229
|
+
if (data.result && trackingData && !options.allow_display_name && !options.require_display_name) {
|
|
230
|
+
tagRangeUtil.addInPlace(
|
|
231
|
+
trackingData.tagRanges,
|
|
232
|
+
new TagRange(0, data.args[0].length - 1, 'limited-chars')
|
|
233
|
+
);
|
|
234
|
+
tagRangeUtil.removeInPlace(trackingData.tagRanges, ['untrusted']);
|
|
235
|
+
|
|
236
|
+
const context = new CallContext(data);
|
|
237
|
+
const event = new PropagationEvent({
|
|
238
|
+
context,
|
|
239
|
+
signature,
|
|
240
|
+
tagRanges: trackingData.tagRanges,
|
|
241
|
+
source: 'O',
|
|
242
|
+
target: 'R',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
event.parents.push(trackingData.event);
|
|
246
|
+
trackingData.event = event;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return patch(isEmail, 'isEmail', post);
|
|
251
|
+
});
|
|
213
252
|
};
|
|
@@ -32,6 +32,7 @@ module.exports = {
|
|
|
32
32
|
isBoolean: 'limited-chars',
|
|
33
33
|
isBtcAddress: 'alphanum-space-hyphen',
|
|
34
34
|
isCreditCard: 'limited-chars',
|
|
35
|
+
isDate: 'limited-chars',
|
|
35
36
|
isDecimal: 'limited-chars',
|
|
36
37
|
// calls toFloat() which calls isFloat() so no need to hook.
|
|
37
38
|
//isDivisibleBy: 'limited-chars',
|
|
@@ -88,5 +89,10 @@ module.exports = {
|
|
|
88
89
|
escape: 'html-encoded'
|
|
89
90
|
// toFloat uses isFloat which is hooked, so no need to do so again
|
|
90
91
|
// toFloat: 'limited-chars'
|
|
92
|
+
},
|
|
93
|
+
customLogic: {
|
|
94
|
+
// this value is not used and it's added just for storing the information
|
|
95
|
+
// about what's patched in one place
|
|
96
|
+
isEmail: 'limited-chars'
|
|
91
97
|
}
|
|
92
98
|
};
|
package/lib/core/hapi/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const constants = {
|
|
|
42
42
|
HAPI_ROUTES: 'hapi-routes', // used to instrument registered routes
|
|
43
43
|
HAPI_ADD_ROUTE: 'hapi-add-route', // used to instrument adding a route
|
|
44
44
|
HAPI_PRE_AUTH: 'hapi-pre-auth', // used to implement hapi onPreAuth hooks
|
|
45
|
+
HAPI_POST_AUTH: 'hapi-post-auth', // uses to implement hapi onPostAuth hooks
|
|
45
46
|
HAPI_PRE_HANDLER: 'hapi-pre-handler', // used to implement hapi onPreHandler hooks
|
|
46
47
|
HAPI_FINISHED: 'hapi-response-finished', // used to implement when hapi response is finished
|
|
47
48
|
HAPI_PRE_RES: 'hapi-pre-response', // used to implement hapi onPreResponse hooks
|
|
@@ -162,12 +163,13 @@ class HapiCore {
|
|
|
162
163
|
return h.continue;
|
|
163
164
|
});
|
|
164
165
|
|
|
165
|
-
server.ext('
|
|
166
|
-
agentEmitter.emit(constants.EVENTS.
|
|
166
|
+
server.ext('onPostAuth', function onPostAuth(request, h) {
|
|
167
|
+
agentEmitter.emit(constants.EVENTS.HAPI_POST_AUTH, {
|
|
167
168
|
request,
|
|
168
169
|
h,
|
|
169
170
|
server
|
|
170
171
|
});
|
|
172
|
+
|
|
171
173
|
// Update the domain's model of the request with body and params since it has been
|
|
172
174
|
// fully parsed and validated
|
|
173
175
|
decorateRequest({
|
|
@@ -176,6 +178,17 @@ class HapiCore {
|
|
|
176
178
|
parameters: request.params,
|
|
177
179
|
query: request.query
|
|
178
180
|
});
|
|
181
|
+
|
|
182
|
+
return h.continue;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
server.ext('onPreHandler', function onPreHandler(request, h) {
|
|
186
|
+
agentEmitter.emit(constants.EVENTS.HAPI_PRE_HANDLER, {
|
|
187
|
+
request,
|
|
188
|
+
h,
|
|
189
|
+
server
|
|
190
|
+
});
|
|
191
|
+
|
|
179
192
|
return h.continue;
|
|
180
193
|
});
|
|
181
194
|
|
|
@@ -16,7 +16,6 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
16
16
|
|
|
17
17
|
const cp = require('child_process');
|
|
18
18
|
const fs = require('fs');
|
|
19
|
-
const os = require('os');
|
|
20
19
|
const path = require('path');
|
|
21
20
|
// according to https://nodejs.org/api/process.html#process_process the process
|
|
22
21
|
// global can be required. We're only doing so here so we can mock it out in tests
|
|
@@ -37,6 +36,21 @@ const SuccessConnectionState = require('./success-connection-state');
|
|
|
37
36
|
|
|
38
37
|
const RESEND_WAIT_MS = 100;
|
|
39
38
|
|
|
39
|
+
/**
|
|
40
|
+
* We might have the /bin/* files in /target directory if we're in
|
|
41
|
+
* development. Packaged/installed agents will have the executables in a
|
|
42
|
+
* top-level directory, e.g. /node_modules/@contrast/agent/bin/*
|
|
43
|
+
* @returns {'target' | '.'}
|
|
44
|
+
*/
|
|
45
|
+
const maybeTargetDir = () => {
|
|
46
|
+
try {
|
|
47
|
+
fs.statSync(path.resolve(__dirname, '..', '..', '..', 'target'));
|
|
48
|
+
return 'target';
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return '.';
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
40
54
|
class Speedracer {
|
|
41
55
|
constructor({ agent, logger }) {
|
|
42
56
|
this.agent = agent;
|
|
@@ -155,7 +169,35 @@ class Speedracer {
|
|
|
155
169
|
this.logger.info('starting contrast-service');
|
|
156
170
|
this.startTime = Date.now();
|
|
157
171
|
|
|
158
|
-
|
|
172
|
+
const speedracerPath = path.resolve(
|
|
173
|
+
__dirname,
|
|
174
|
+
'..',
|
|
175
|
+
'..',
|
|
176
|
+
'..',
|
|
177
|
+
maybeTargetDir(),
|
|
178
|
+
'bin',
|
|
179
|
+
`contrast-service-${process.platform}-${process.arch}`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
fs.statSync(speedracerPath);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.logger.error(
|
|
186
|
+
'unable to locate a speedracer binary for the current platform (%s) and architecture (%s)',
|
|
187
|
+
process.platform,
|
|
188
|
+
process.arch
|
|
189
|
+
);
|
|
190
|
+
return Promise.reject();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.serviceProcess = cp.spawn(speedracerPath, {
|
|
194
|
+
env: {
|
|
195
|
+
...process.env,
|
|
196
|
+
CONTRAST_CONFIG_PATH: this.agent.config.configFile
|
|
197
|
+
},
|
|
198
|
+
stdio: 'ignore',
|
|
199
|
+
windowsHide: true
|
|
200
|
+
});
|
|
159
201
|
|
|
160
202
|
this.serviceProcess.on('error', (err) => {
|
|
161
203
|
this.serviceProcess = null;
|
|
@@ -262,39 +304,6 @@ class Speedracer {
|
|
|
262
304
|
}
|
|
263
305
|
}
|
|
264
306
|
|
|
265
|
-
/**
|
|
266
|
-
* The start command for spawning the speedracer
|
|
267
|
-
*/
|
|
268
|
-
get startCommand() {
|
|
269
|
-
return path.resolve(
|
|
270
|
-
__dirname,
|
|
271
|
-
path.join(
|
|
272
|
-
'..',
|
|
273
|
-
'..',
|
|
274
|
-
'..',
|
|
275
|
-
utils.maybeTargetDir(),
|
|
276
|
-
'bin',
|
|
277
|
-
utils.getOsDir(),
|
|
278
|
-
'contrast-service'
|
|
279
|
-
)
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* The start options for spawning the speedracer child process. We provide the
|
|
285
|
-
* service the location of our config file via environment variable.
|
|
286
|
-
*/
|
|
287
|
-
get startOptions() {
|
|
288
|
-
return {
|
|
289
|
-
env: {
|
|
290
|
-
...process.env,
|
|
291
|
-
CONTRAST_CONFIG_PATH: this.agent.config.configFile
|
|
292
|
-
},
|
|
293
|
-
stdio: 'ignore',
|
|
294
|
-
windowsHide: true
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
307
|
/**
|
|
299
308
|
* GRPC is enabled when `agent.service.grpc` is not false, e.g., undefined
|
|
300
309
|
* defaults to enabled, and `agent.service.socket` is not configured
|
|
@@ -314,39 +323,4 @@ class Speedracer {
|
|
|
314
323
|
}
|
|
315
324
|
}
|
|
316
325
|
|
|
317
|
-
const utils = {
|
|
318
|
-
/**
|
|
319
|
-
* We might have the /bin/* files in /target directory if we're in
|
|
320
|
-
* development. Packaged/installed agents will have the executables in a
|
|
321
|
-
* top-level directory, e.g. /node_modules/@contrast/agent/bin/*
|
|
322
|
-
* @returns {String}
|
|
323
|
-
*/
|
|
324
|
-
maybeTargetDir() {
|
|
325
|
-
try {
|
|
326
|
-
fs.statSync(
|
|
327
|
-
path.resolve(__dirname, path.join('..', '..', '..', 'target'))
|
|
328
|
-
);
|
|
329
|
-
return 'target';
|
|
330
|
-
} catch (error) {
|
|
331
|
-
return '';
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
/**
|
|
335
|
-
* Map platform values to folder names based on OS. The folder names are
|
|
336
|
-
* determined by the packaging of the S-R artifacts and how they unzip.
|
|
337
|
-
* @returns {String}
|
|
338
|
-
*/
|
|
339
|
-
getOsDir() {
|
|
340
|
-
switch (os.platform()) {
|
|
341
|
-
case 'linux':
|
|
342
|
-
return 'linux';
|
|
343
|
-
case 'darwin':
|
|
344
|
-
return 'mac';
|
|
345
|
-
case 'win32':
|
|
346
|
-
return 'windows';
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
326
|
module.exports = Speedracer;
|
|
352
|
-
module.exports.utils = utils;
|
package/lib/util/heap-dump.js
CHANGED
|
@@ -12,51 +12,63 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
12
12
|
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
16
17
|
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const v8 = require('v8');
|
|
17
20
|
const logger = require('../core/logger')('contrast:heapDump');
|
|
18
|
-
const heapUtil = module.exports;
|
|
19
|
-
let heapdump;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* @param {string} localPath location from config.path
|
|
24
|
+
*/
|
|
25
|
+
const writeHeapSnapshot = function writeHeapSnapshot(localPath) {
|
|
26
|
+
const dumpPath = path.join(process.cwd(), localPath);
|
|
27
|
+
|
|
28
|
+
fs.mkdir(dumpPath, { recursive: true }, (err) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
logger.error('Unable to create directory for heap snapshots: %o', err);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// create dump at ${path}/${time}.heapdump
|
|
35
|
+
const filename = path.format({
|
|
36
|
+
dir: dumpPath,
|
|
37
|
+
name: `${Date.now()}-contrast`,
|
|
38
|
+
ext: '.heapsnapshot',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
logger.info('Writing heap snapshot at %s', filename);
|
|
42
|
+
v8.writeHeapSnapshot(filename);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* setup x heap dumps to be created every y seconds after z seconds.
|
|
23
48
|
* @param {Object} config config blob from config/options.js
|
|
24
|
-
*
|
|
49
|
+
* @param {boolean} config.enable
|
|
50
|
+
* @param {string} config.path
|
|
51
|
+
* @param {number} config.count x
|
|
52
|
+
* @param {number} config.window_ms y
|
|
53
|
+
* @param {number} config.delay_ms z
|
|
25
54
|
*/
|
|
26
|
-
|
|
55
|
+
const init = function init(config) {
|
|
27
56
|
if (!config.enable) return;
|
|
28
|
-
// NODE-1200: make this optional based on if config
|
|
29
|
-
// bit is flipped
|
|
30
|
-
heapdump = require('@contrast/heapdump');
|
|
31
57
|
|
|
32
58
|
setTimeout(() => {
|
|
33
59
|
let count = 0;
|
|
34
|
-
|
|
60
|
+
|
|
61
|
+
const interval = setInterval(() => {
|
|
35
62
|
if (count >= config.count) {
|
|
36
|
-
clearInterval(
|
|
63
|
+
clearInterval(interval);
|
|
37
64
|
return;
|
|
38
65
|
}
|
|
39
|
-
|
|
66
|
+
|
|
67
|
+
writeHeapSnapshot(config.path);
|
|
40
68
|
|
|
41
69
|
count++;
|
|
42
70
|
}, config.window_ms);
|
|
43
71
|
}, config.delay_ms).unref();
|
|
44
72
|
};
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
// create directory in cwd
|
|
48
|
-
dumpPath = `${path.join(process.cwd(), dumpPath)}`;
|
|
49
|
-
fs.mkdir(dumpPath, (err) => {
|
|
50
|
-
if (!err || (err && err.code == 'EEXIST')) {
|
|
51
|
-
// create dump at ${path}/${time}.heapdump
|
|
52
|
-
const fileName = `${path.join(
|
|
53
|
-
dumpPath,
|
|
54
|
-
String(Date.now())
|
|
55
|
-
)}-contrast.heapsnapshot`;
|
|
56
|
-
logger.info(`Building heap snapshot at ${fileName}`);
|
|
57
|
-
heapdump.writeSnapshot(fileName);
|
|
58
|
-
} else {
|
|
59
|
-
logger.error(`Unable to create directory for heap snapshots: ${err}`);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
};
|
|
74
|
+
module.exports = { writeHeapSnapshot, init };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.22.0",
|
|
4
4
|
"description": "Node.js security instrumentation by Contrast Security",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -76,13 +76,12 @@
|
|
|
76
76
|
"@babel/template": "^7.10.4",
|
|
77
77
|
"@babel/traverse": "^7.12.1",
|
|
78
78
|
"@babel/types": "^7.12.1",
|
|
79
|
-
"@contrast/agent-lib": "^4.
|
|
80
|
-
"@contrast/distringuish-prebuilt": "^3.0
|
|
79
|
+
"@contrast/agent-lib": "^4.3.0",
|
|
80
|
+
"@contrast/distringuish-prebuilt": "^3.2.0",
|
|
81
81
|
"@contrast/flat": "^4.1.1",
|
|
82
|
-
"@contrast/fn-inspect": "^3.
|
|
83
|
-
"@contrast/heapdump": "^1.1.0",
|
|
82
|
+
"@contrast/fn-inspect": "^3.1.0",
|
|
84
83
|
"@contrast/protobuf-api": "^3.2.5",
|
|
85
|
-
"@contrast/require-hook": "^3.
|
|
84
|
+
"@contrast/require-hook": "^3.2.1",
|
|
86
85
|
"@contrast/synchronous-source-maps": "^1.1.0",
|
|
87
86
|
"amqp-connection-manager": "^3.2.2",
|
|
88
87
|
"amqplib": "^0.8.0",
|