@contrast/agentify 1.0.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/LICENSE +12 -0
- package/README.md +3 -0
- package/lib/contrast-methods.js +45 -0
- package/lib/contrast-methods.test.js +72 -0
- package/lib/function-hooks.js +118 -0
- package/lib/function-hooks.test.js +175 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +113 -0
- package/lib/index.test.js +89 -0
- package/lib/instrumentation-locks.js +36 -0
- package/lib/instrumentation-locks.test.js +55 -0
- package/lib/rewrite-hooks.js +38 -0
- package/lib/rewrite-hooks.test.js +76 -0
- package/lib/sources.js +63 -0
- package/lib/sources.test.js +196 -0
- package/package.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright: 2022 Contrast Security, Inc
|
|
2
|
+
Contact: support@contrastsecurity.com
|
|
3
|
+
License: Commercial
|
|
4
|
+
|
|
5
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
6
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
7
|
+
made available through public repositories, use of this Software is subject to
|
|
8
|
+
the applicable End User Licensing Agreement found at
|
|
9
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
10
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
11
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
12
|
+
way not consistent with the End User License Agreement.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function(deps) {
|
|
4
|
+
/**
|
|
5
|
+
* Components e.g. Assess, Protect, can
|
|
6
|
+
* 1) configure rewriter to add desired ContrastMethod functions to source code
|
|
7
|
+
* 2) patch those functions on `contrastMethods.api` in order to add instrumentation
|
|
8
|
+
*/
|
|
9
|
+
const contrastMethods = deps.contrastMethods = {
|
|
10
|
+
// TODO: Assess will require add'l methods
|
|
11
|
+
// TODO: ESM will require add'l methods
|
|
12
|
+
api: {
|
|
13
|
+
eval(v) {
|
|
14
|
+
return v;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
installed: false,
|
|
18
|
+
getGlobal() {
|
|
19
|
+
return global;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
contrastMethods.install = function() {
|
|
24
|
+
if (contrastMethods.installed) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const global = contrastMethods.getGlobal();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
Object.defineProperty(global, 'ContrastMethods', {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: false,
|
|
34
|
+
value: contrastMethods.api
|
|
35
|
+
});
|
|
36
|
+
contrastMethods.installed = true;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// We should never expect this since the installation process is well
|
|
39
|
+
// controlled, but still we should have the defensive code.
|
|
40
|
+
deps.logger.error({ err }, 'Unable to install global.ContrastMethods');
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return deps.contrastMethods;
|
|
45
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
|
|
6
|
+
describe('agentify contrast-methods', function() {
|
|
7
|
+
let contrastMethods;
|
|
8
|
+
let core;
|
|
9
|
+
|
|
10
|
+
beforeEach(function() {
|
|
11
|
+
const mocks = require('../../test/mocks');
|
|
12
|
+
core = mocks.core();
|
|
13
|
+
core.logger = mocks.logger();
|
|
14
|
+
contrastMethods = require('./contrast-methods')(core);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('api.eval()', function() {
|
|
18
|
+
it('acts as identity function', function() {
|
|
19
|
+
[{}, null, 1, 'two', undefined].forEach(v => {
|
|
20
|
+
expect(v).to.equal(contrastMethods.api.eval(v));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('.getGlobal', function() {
|
|
26
|
+
it('returns global :)', function() {
|
|
27
|
+
expect(contrastMethods.getGlobal()).to.equal(global);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('.install', function() {
|
|
32
|
+
let globalMock;
|
|
33
|
+
let contrastMethods;
|
|
34
|
+
|
|
35
|
+
beforeEach(function() {
|
|
36
|
+
contrastMethods = require('./contrast-methods')(core);
|
|
37
|
+
globalMock = {};
|
|
38
|
+
sinon.stub(contrastMethods, 'getGlobal').returns(globalMock);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('installs on global', function() {
|
|
42
|
+
expect(globalMock.ContrastMethods).not.to.exist;
|
|
43
|
+
contrastMethods.install();
|
|
44
|
+
expect(globalMock.ContrastMethods).to.exist;
|
|
45
|
+
expect(() => {
|
|
46
|
+
delete globalMock.ContrastMethods;
|
|
47
|
+
}).throw(/Cannot delete property/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('installs just once', function() {
|
|
51
|
+
expect(globalMock.ContrastMethods).not.to.exist;
|
|
52
|
+
contrastMethods.install();
|
|
53
|
+
contrastMethods.install();
|
|
54
|
+
expect(globalMock.ContrastMethods).to.equal(contrastMethods.api);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('installation failure handing', function() {
|
|
58
|
+
beforeEach(function() {
|
|
59
|
+
Object.defineProperty(globalMock, 'ContrastMethods', {
|
|
60
|
+
configurable: false,
|
|
61
|
+
value: 'does not matter for test'
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('logs error when global.ContrastMethods cannot be redefined', function() {
|
|
65
|
+
contrastMethods.install();
|
|
66
|
+
const [{ err }, description] = core.logger.error.getCall(0).args;
|
|
67
|
+
expect(description).to.eql('Unable to install global.ContrastMethods');
|
|
68
|
+
expect(err.message).to.eql('Cannot redefine property: ContrastMethods');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const IS_CLASS_REGEX = /^class\W/;
|
|
4
|
+
const IS_FUNCTION_REGEX = /^(async\s+)?function\W/;
|
|
5
|
+
const IS_RETURN_REGEX = /^return\W/;
|
|
6
|
+
const METHOD_CONTEXT = 'function _contrast_';
|
|
7
|
+
const FUNCTION_CONTEXT = 'const _contrast_fn = ';
|
|
8
|
+
|
|
9
|
+
module.exports = function (deps) {
|
|
10
|
+
const {
|
|
11
|
+
rewriter,
|
|
12
|
+
logger,
|
|
13
|
+
patcher,
|
|
14
|
+
patcher: { hookedFunctions },
|
|
15
|
+
} = deps;
|
|
16
|
+
|
|
17
|
+
const toString = patcher.unwrap(Function.prototype.toString);
|
|
18
|
+
|
|
19
|
+
const functionHooks = (deps.functionHooks = {
|
|
20
|
+
installed: false,
|
|
21
|
+
cache: new WeakMap(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create some code context around a function to make it syntactically valid
|
|
26
|
+
* the three general types of syntactic function definitions are:
|
|
27
|
+
* - functions (declared with the function keyword)
|
|
28
|
+
* - arrow functions
|
|
29
|
+
* - class methods
|
|
30
|
+
* @param {string} code the user code
|
|
31
|
+
* @returns {string} the contextualized function
|
|
32
|
+
*/
|
|
33
|
+
functionHooks.contextualizeFunction = (code) => {
|
|
34
|
+
// Classes themselves are functions, so we need to exit early to avoid
|
|
35
|
+
// clobbering the `class` keyword.
|
|
36
|
+
if (IS_CLASS_REGEX.exec(code)) return code;
|
|
37
|
+
const [orig] = code.split('{');
|
|
38
|
+
|
|
39
|
+
let lineOne = orig;
|
|
40
|
+
|
|
41
|
+
// if the function is a return, we don't need to add context.
|
|
42
|
+
if (!IS_RETURN_REGEX.exec(lineOne)) {
|
|
43
|
+
// When stringified, class methods look like normal function without the
|
|
44
|
+
// `function` keyword. We can normalize this by prepending `function`.
|
|
45
|
+
if (!IS_FUNCTION_REGEX.exec(lineOne) && lineOne.indexOf('=>') === -1) {
|
|
46
|
+
lineOne = `${METHOD_CONTEXT}${lineOne}`;
|
|
47
|
+
}
|
|
48
|
+
lineOne = `${FUNCTION_CONTEXT}${lineOne}`;
|
|
49
|
+
|
|
50
|
+
code = code.replace(orig, lineOne);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return code;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the rewritten function code. If an error occurs during rewriting,
|
|
58
|
+
* we log the error and code information and return the original code string.
|
|
59
|
+
* @param {string} code string value of the JavaScript function
|
|
60
|
+
* @returns {string} the code rewritten to remove Contrast tokens
|
|
61
|
+
*/
|
|
62
|
+
functionHooks.unwrite = function (code) {
|
|
63
|
+
// cannot parse lone function expressions that are not part of an assignment.
|
|
64
|
+
try {
|
|
65
|
+
let unwritten = functionHooks.contextualizeFunction(code);
|
|
66
|
+
unwritten = rewriter.unwrite(unwritten);
|
|
67
|
+
unwritten = unwritten.replace(METHOD_CONTEXT, '');
|
|
68
|
+
unwritten = unwritten.replace(FUNCTION_CONTEXT, '');
|
|
69
|
+
unwritten = unwritten.replace(/;$/, ''); // removes trailing semicolon
|
|
70
|
+
return unwritten;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.warn({ err, code }, 'Failed to unwrite function code');
|
|
73
|
+
return code;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
functionHooks.install = function () {
|
|
78
|
+
if (deps.functionHooks.installed) return;
|
|
79
|
+
|
|
80
|
+
logger.debug('deploying function toString patch');
|
|
81
|
+
|
|
82
|
+
patcher.patch(Function.prototype, 'toString', {
|
|
83
|
+
name: 'Function.prototype.toString',
|
|
84
|
+
patchType: 'function-hooks',
|
|
85
|
+
around(orig, data) {
|
|
86
|
+
// rewriting is expensive, but should caching be configurable?
|
|
87
|
+
if (functionHooks.cache.has(data.obj)) {
|
|
88
|
+
return functionHooks.cache.get(data.obj);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let code;
|
|
92
|
+
if (hookedFunctions.has(data.obj)) {
|
|
93
|
+
code = toString.call(hookedFunctions.get(data.obj).fn);
|
|
94
|
+
} else {
|
|
95
|
+
code = orig();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let result = code;
|
|
99
|
+
|
|
100
|
+
if (code.indexOf('ContrastMethods')) {
|
|
101
|
+
const unwritten = functionHooks.unwrite(code);
|
|
102
|
+
functionHooks.cache.set(data.obj, unwritten);
|
|
103
|
+
result = unwritten;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
functionHooks.installed = true;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
functionHooks.uninstall = function () {
|
|
114
|
+
Function.prototype.toString = toString;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return functionHooks;
|
|
118
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const functionHooks = require('./function-hooks');
|
|
5
|
+
|
|
6
|
+
describe('agentify function-hooks', function () {
|
|
7
|
+
let coreMock;
|
|
8
|
+
|
|
9
|
+
beforeEach(function () {
|
|
10
|
+
const mocks = require('../../test/mocks');
|
|
11
|
+
|
|
12
|
+
coreMock = mocks.core();
|
|
13
|
+
coreMock.patcher = mocks.patcher();
|
|
14
|
+
coreMock.logger = mocks.logger();
|
|
15
|
+
coreMock.depHooks = mocks.depHooks();
|
|
16
|
+
coreMock.scopes = mocks.scopes();
|
|
17
|
+
coreMock.rewriter = mocks.rewriter();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should install function hooks', function () {
|
|
21
|
+
functionHooks(coreMock);
|
|
22
|
+
coreMock.functionHooks.install();
|
|
23
|
+
expect(coreMock.functionHooks.installed).to.be.true;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should skip if it is already installed', function () {
|
|
27
|
+
functionHooks(coreMock);
|
|
28
|
+
coreMock.functionHooks.installed = true;
|
|
29
|
+
coreMock.functionHooks.install();
|
|
30
|
+
|
|
31
|
+
expect(coreMock.logger.debug).to.not.have.been.called;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return cached value when the function is already called', function () {
|
|
35
|
+
const functionA = function sum(a, b) {
|
|
36
|
+
eval(global.ContrastMethods.eval('2 + 2'));
|
|
37
|
+
return a + b;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const cachedValue = 'cachedValue';
|
|
41
|
+
|
|
42
|
+
functionHooks(coreMock);
|
|
43
|
+
|
|
44
|
+
coreMock.functionHooks.cache.set(functionA, cachedValue);
|
|
45
|
+
|
|
46
|
+
const data = {
|
|
47
|
+
obj: functionA,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let result;
|
|
51
|
+
coreMock.patcher.patch.callsFake((prototype, method, options) => {
|
|
52
|
+
result = options.around(() => ({}), data);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
coreMock.functionHooks.install();
|
|
56
|
+
|
|
57
|
+
expect(result).to.eql(cachedValue);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should call the function itself when the function is not hooked', function () {
|
|
61
|
+
const functionA = function sum(a, b) {
|
|
62
|
+
eval(global.ContrastMethods.eval('2 + 2'));
|
|
63
|
+
return a + b;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
functionHooks(coreMock);
|
|
67
|
+
|
|
68
|
+
const data = {
|
|
69
|
+
obj: functionA,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let result;
|
|
73
|
+
coreMock.patcher.patch.callsFake((prototype, method, options) => {
|
|
74
|
+
result = options.around(functionA.toString.bind(functionA), data);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const unwriteResult = `function sum(a, b) {
|
|
78
|
+
eval(2 + 2);
|
|
79
|
+
return a + b;
|
|
80
|
+
}`;
|
|
81
|
+
|
|
82
|
+
coreMock.rewriter.unwrite.callsFake((data) => unwriteResult);
|
|
83
|
+
|
|
84
|
+
coreMock.functionHooks.install();
|
|
85
|
+
|
|
86
|
+
expect(result).to.eql(unwriteResult);
|
|
87
|
+
expect(coreMock.functionHooks.cache.get(data.obj)).to.eql(unwriteResult);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should the call the original function of the hooked function', function () {
|
|
91
|
+
const functionA = function sum(a, b) {
|
|
92
|
+
eval(global.ContrastMethods.eval('2 + 2'));
|
|
93
|
+
return a + b;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const functionB = function sum(a, b) {
|
|
97
|
+
eval(global.ContrastMethods.eval('2 + 2'));
|
|
98
|
+
return a + b + 2;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
functionHooks(coreMock);
|
|
102
|
+
|
|
103
|
+
const weekMap = coreMock.patcher.hookedFunctions;
|
|
104
|
+
weekMap.set(functionA, { fn: functionB });
|
|
105
|
+
|
|
106
|
+
const data = {
|
|
107
|
+
obj: functionA,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let result;
|
|
111
|
+
coreMock.patcher.patch.callsFake((prototype, method, options) => {
|
|
112
|
+
result = options.around(functionA.toString.bind(functionA), data);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const unwriteResult = `function sum(a, b) {
|
|
116
|
+
eval(2 + 2);
|
|
117
|
+
return a + b + 2;
|
|
118
|
+
}`;
|
|
119
|
+
|
|
120
|
+
coreMock.rewriter.unwrite.callsFake((data) => unwriteResult);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
coreMock.functionHooks.install();
|
|
124
|
+
|
|
125
|
+
expect(result).to.eql(unwriteResult);
|
|
126
|
+
expect(coreMock.functionHooks.cache.get(data.obj)).to.eql(unwriteResult);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should log warning message when there is a problem unwriting the code', function () {
|
|
130
|
+
const err = new Error('Error');
|
|
131
|
+
const functionA = function sum(a, b) {
|
|
132
|
+
eval(global.ContrastMethods.eval('2 + 2'));
|
|
133
|
+
return a + b;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
functionHooks(coreMock);
|
|
137
|
+
|
|
138
|
+
const data = {
|
|
139
|
+
obj: functionA,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
coreMock.patcher.patch.callsFake((prototype, method, options) => {
|
|
143
|
+
options.around(functionA.toString.bind(functionA), data);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
coreMock.rewriter.unwrite.throws(err);
|
|
147
|
+
|
|
148
|
+
coreMock.functionHooks.install();
|
|
149
|
+
|
|
150
|
+
expect(coreMock.logger.warn).to.have.been.calledWith({ err, code: functionA.toString() }, 'Failed to unwrite function code');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should call the original toString if the function is not hooked', function() {
|
|
154
|
+
const functionA = () => 1;
|
|
155
|
+
functionHooks(coreMock);
|
|
156
|
+
coreMock.functionHooks.install();
|
|
157
|
+
|
|
158
|
+
const resultOfA = functionA.toString();
|
|
159
|
+
|
|
160
|
+
expect(resultOfA).to.eqls('() => 1');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should uninstall the hooks', function() {
|
|
164
|
+
const { toString } = Function.prototype;
|
|
165
|
+
const functionA = () => 1;
|
|
166
|
+
functionHooks(coreMock);
|
|
167
|
+
coreMock.functionHooks.install();
|
|
168
|
+
coreMock.functionHooks.uninstall();
|
|
169
|
+
|
|
170
|
+
const resultOfA = functionA.toString();
|
|
171
|
+
|
|
172
|
+
expect(resultOfA).to.eqls('() => 1');
|
|
173
|
+
expect(Function.prototype.toString).to.eqls(toString);
|
|
174
|
+
});
|
|
175
|
+
});
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import RequireHook from '@contrast/require-hook';
|
|
2
|
+
import { Logger } from '@contrast/logger';
|
|
3
|
+
|
|
4
|
+
interface AgentifyOptions {
|
|
5
|
+
install: boolean;
|
|
6
|
+
svcList: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AgentOptions<T> {
|
|
10
|
+
install: boolean;
|
|
11
|
+
preRunMain: PreRunMain<T>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare class Agent<T> {
|
|
15
|
+
constructor(deps: T, opts: AgentOptions<T>);
|
|
16
|
+
hookRunMain(): void;
|
|
17
|
+
handleInstallFailure(err: Error): Promise<void>;
|
|
18
|
+
install(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PreRunMain<T> {
|
|
22
|
+
(core: T, agent: Agent<T>): void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Agentify<T> {
|
|
26
|
+
(preRunMain: PreRunMain<T>, opts?: AgentifyOptions): Agent<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Core<T> {
|
|
30
|
+
readonly depHooks: RequireHook;
|
|
31
|
+
readonly logger: Logger;
|
|
32
|
+
agentify: Agentify<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
declare function init<T>(core: Core<T>): Agentify<T>;
|
|
36
|
+
|
|
37
|
+
export = init;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Module = require('module');
|
|
4
|
+
|
|
5
|
+
const defaultOpts = {
|
|
6
|
+
install: true,
|
|
7
|
+
svcList: [
|
|
8
|
+
'reporter',
|
|
9
|
+
'contrastMethods',
|
|
10
|
+
'instrumentationLocks',
|
|
11
|
+
'sources',
|
|
12
|
+
'protect',
|
|
13
|
+
'depHooks',
|
|
14
|
+
'rewriteHooks',
|
|
15
|
+
'functionHooks',
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = function(deps) {
|
|
20
|
+
// compose add'l local services
|
|
21
|
+
require('./sources')(deps);
|
|
22
|
+
require('./contrast-methods')(deps);
|
|
23
|
+
require('./instrumentation-locks')(deps);
|
|
24
|
+
require('./function-hooks')(deps);
|
|
25
|
+
require('./rewrite-hooks')(deps);
|
|
26
|
+
// TODO: NODE-2229
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The interface is a function, which when called, will hook runMain
|
|
30
|
+
* @param {function} preRunMain
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
*/
|
|
33
|
+
function agentify(preRunMain, opts = defaultOpts) {
|
|
34
|
+
if (typeof preRunMain !== 'function') {
|
|
35
|
+
throw new Error('Invalid usage: first argument must be a function');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
opts.preRunMain = preRunMain;
|
|
39
|
+
|
|
40
|
+
if (opts !== defaultOpts) {
|
|
41
|
+
opts = { ...defaultOpts, ...opts };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Agent(deps, opts);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
deps.agentify = agentify;
|
|
48
|
+
|
|
49
|
+
return agentify;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class Agent {
|
|
53
|
+
/**
|
|
54
|
+
*
|
|
55
|
+
* @param {object} deps dependencies
|
|
56
|
+
* @param {object} opts
|
|
57
|
+
* @param {function} opts.preRunMain custom function for registering services
|
|
58
|
+
* @param {boolean} opts.install whether to automatically install
|
|
59
|
+
*/
|
|
60
|
+
constructor(deps, opts) {
|
|
61
|
+
const self = this;
|
|
62
|
+
const { config, logger } = deps;
|
|
63
|
+
|
|
64
|
+
this.deps = deps;
|
|
65
|
+
this.opts = opts;
|
|
66
|
+
this.runMain = Module.runMain;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* This is one side effect that will always occur, even with `install: false`.
|
|
70
|
+
* The act of calling `agentify()` is enough to cause this side effect, by design.
|
|
71
|
+
*/
|
|
72
|
+
Module.runMain = async function(...args) {
|
|
73
|
+
try {
|
|
74
|
+
logger.info('Starting the Contrast agent');
|
|
75
|
+
logger.debug({ config }, 'Agent configuration');
|
|
76
|
+
|
|
77
|
+
await opts.preRunMain(deps);
|
|
78
|
+
if (opts.install) {
|
|
79
|
+
await self.install();
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
await self.handleInstallFailure(err);
|
|
83
|
+
}
|
|
84
|
+
return self.runMain.apply(this, args);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Original `runMain` will get called after this.
|
|
90
|
+
*
|
|
91
|
+
* TODO: Consider proper UNINSTALLATION and normal startup w/o agent
|
|
92
|
+
*
|
|
93
|
+
* @param {error} err Error thrown during install
|
|
94
|
+
*/
|
|
95
|
+
async handleInstallFailure(err) {
|
|
96
|
+
this.deps.logger.error(
|
|
97
|
+
{ err },
|
|
98
|
+
'A fatal agent installation error has occurred. The application will be run without instrumentation.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async install() {
|
|
103
|
+
for (const svcName of this.opts.svcList) {
|
|
104
|
+
this.deps.logger.trace('installing service: %s', svcName);
|
|
105
|
+
const svc = this.deps[svcName];
|
|
106
|
+
if (svc && svc.install) {
|
|
107
|
+
await svc.install();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports.Agent = Agent;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chai = require('chai');
|
|
4
|
+
const { expect } = chai;
|
|
5
|
+
const sinon = require('sinon');
|
|
6
|
+
|
|
7
|
+
describe('agentify', function() {
|
|
8
|
+
const Module = require('module');
|
|
9
|
+
let core;
|
|
10
|
+
let agentify;
|
|
11
|
+
let runMain;
|
|
12
|
+
|
|
13
|
+
beforeEach(function() {
|
|
14
|
+
const factory = require('.');
|
|
15
|
+
const mocks = require('../../test/mocks');
|
|
16
|
+
|
|
17
|
+
core = mocks.core();
|
|
18
|
+
core.logger = mocks.logger();
|
|
19
|
+
core.depHooks = mocks.depHooks();
|
|
20
|
+
core.scopes = mocks.scopes();
|
|
21
|
+
core.patcher = mocks.patcher();
|
|
22
|
+
agentify = factory(core);
|
|
23
|
+
runMain = sinon.stub(Module, 'runMain');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('factory', function() {
|
|
27
|
+
it('invoking with callback will initialize and patch runMain', async function() {
|
|
28
|
+
let ranFlag;
|
|
29
|
+
|
|
30
|
+
agentify((deps) => {
|
|
31
|
+
expect(deps).to.equal(core);
|
|
32
|
+
ranFlag = true;
|
|
33
|
+
}, {
|
|
34
|
+
svcList: ['depHooks']
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await Module.runMain();
|
|
38
|
+
|
|
39
|
+
expect(ranFlag).to.be.true;
|
|
40
|
+
expect(runMain).to.be.called;
|
|
41
|
+
expect(core.depHooks.install).to.have.been.called;
|
|
42
|
+
expect(core.logger.error).not.to.been.called;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('will not run install methods opts = { install: false }', async function() {
|
|
46
|
+
const a = agentify(() => {}, { install: false });
|
|
47
|
+
sinon.spy(a, 'install');
|
|
48
|
+
|
|
49
|
+
await Module.runMain();
|
|
50
|
+
|
|
51
|
+
expect(a.install).not.to.have.been.called;
|
|
52
|
+
expect(core.logger.error).not.called;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles errors in pre runMain hook (init)', async function() {
|
|
56
|
+
const err = new Error('bonk');
|
|
57
|
+
const a = agentify(() => {
|
|
58
|
+
throw err;
|
|
59
|
+
});
|
|
60
|
+
sinon.spy(a, 'install');
|
|
61
|
+
sinon.spy(a, 'handleInstallFailure');
|
|
62
|
+
|
|
63
|
+
await Module.runMain();
|
|
64
|
+
|
|
65
|
+
expect(a.handleInstallFailure).to.have.been.calledWith(err);
|
|
66
|
+
expect(a.install).to.not.have.been.called;
|
|
67
|
+
expect(core.logger.error).calledWith({ err }, 'A fatal agent installation error has occurred. The application will be run without instrumentation.');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
//
|
|
71
|
+
it('instantiation handles "unregistered" services', async function() {
|
|
72
|
+
agentify((deps) => 0, {
|
|
73
|
+
install: true,
|
|
74
|
+
svcList: ['foo']
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Having non-existent service in list is handled during initialization
|
|
78
|
+
await Module.runMain();
|
|
79
|
+
|
|
80
|
+
expect(core.logger.error).not.called;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('requires init function arg', function() {
|
|
84
|
+
expect(() => {
|
|
85
|
+
agentify('not a function');
|
|
86
|
+
}).to.throw('Invalid usage: first argument must be a function');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Module = require('module');
|
|
4
|
+
|
|
5
|
+
module.exports = function(deps) {
|
|
6
|
+
const {
|
|
7
|
+
patcher: { patch },
|
|
8
|
+
scopes: { instrumentation }
|
|
9
|
+
} = deps;
|
|
10
|
+
|
|
11
|
+
deps.instrumentationLocks = {
|
|
12
|
+
install() {
|
|
13
|
+
const name = 'Module.prototype.require';
|
|
14
|
+
const store = {
|
|
15
|
+
name: `${name}`,
|
|
16
|
+
lock: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// `require` has sinks and propagators that run
|
|
20
|
+
patch(Module.prototype, 'require', {
|
|
21
|
+
name,
|
|
22
|
+
patchType: 'instrumentation-lock',
|
|
23
|
+
around(next, data) {
|
|
24
|
+
if (!instrumentation.isLocked()) {
|
|
25
|
+
return instrumentation.run(store, next);
|
|
26
|
+
}
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// likely more to come e.g. v4 deadzones
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return deps.instrumentationLocks;
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const Module = require('module');
|
|
6
|
+
const instrumentationLocks = require('./instrumentation-locks');
|
|
7
|
+
const mocks = require('../../test/mocks');
|
|
8
|
+
|
|
9
|
+
describe('agentify instrumentation-locks', function () {
|
|
10
|
+
let core, storeSpy;
|
|
11
|
+
|
|
12
|
+
beforeEach(function () {
|
|
13
|
+
core = mocks.core();
|
|
14
|
+
core.logger = mocks.logger();
|
|
15
|
+
core.scopes = require('@contrast/scopes')(core);
|
|
16
|
+
core.patcher = require('@contrast/patcher')(core);
|
|
17
|
+
storeSpy = sinon.stub();
|
|
18
|
+
|
|
19
|
+
const origRequire = Module.prototype.require;
|
|
20
|
+
sinon.stub(Module.prototype, 'require').callsFake(function (...args) {
|
|
21
|
+
storeSpy(core.scopes.instrumentation.getStore());
|
|
22
|
+
return origRequire.call(this, ...args);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
instrumentationLocks(core);
|
|
26
|
+
core.instrumentationLocks.install();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('require gets called with instrumentation locked', function () {
|
|
30
|
+
// side effects are in place for this to exercise properly
|
|
31
|
+
require('crypto');
|
|
32
|
+
expect(storeSpy).to.have.been.calledWith({
|
|
33
|
+
lock: true,
|
|
34
|
+
name: 'Module.prototype.require',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('will not run with lock if already locked', function () {
|
|
39
|
+
// side effects are in place for this to exercise properly
|
|
40
|
+
core.scopes.instrumentation.run({ lock: true, name: 'test' }, () => {
|
|
41
|
+
// target here is arbitrary
|
|
42
|
+
require('events');
|
|
43
|
+
|
|
44
|
+
expect(storeSpy).to.have.been.calledWith({
|
|
45
|
+
lock: true,
|
|
46
|
+
name: 'test',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(storeSpy).to.not.have.been.calledWith({
|
|
50
|
+
lock: true,
|
|
51
|
+
name: 'Module.prototype.require',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Module = require('module');
|
|
4
|
+
|
|
5
|
+
module.exports = function(deps) {
|
|
6
|
+
const origCompile = Module.prototype._compile;
|
|
7
|
+
|
|
8
|
+
deps.rewriteHooks = {
|
|
9
|
+
|
|
10
|
+
install() {
|
|
11
|
+
if (!deps.config.agent.node.enable_rewrite) return;
|
|
12
|
+
|
|
13
|
+
Module.prototype._compile = function(content, filename) {
|
|
14
|
+
let compiled;
|
|
15
|
+
const { code } = deps.rewriter.rewrite(content, { filename });
|
|
16
|
+
try {
|
|
17
|
+
compiled = origCompile.call(this, code, filename);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
deps.logger.error(
|
|
20
|
+
{ err },
|
|
21
|
+
'Failed to compile rewritten code for %s, rewritten code %s, compiling original code.',
|
|
22
|
+
filename,
|
|
23
|
+
code
|
|
24
|
+
);
|
|
25
|
+
compiled = origCompile.call(this, content, filename);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return compiled;
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
restore() {
|
|
33
|
+
Module.prototype._compile = origCompile;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return deps.rewriteHooks;
|
|
38
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const Module = require('module');
|
|
6
|
+
|
|
7
|
+
const testCode = require.resolve('../test/resources/file');
|
|
8
|
+
|
|
9
|
+
describe('agentify rewrite-hooks', function() {
|
|
10
|
+
let core, rewriteHooks, origCompileSpy;
|
|
11
|
+
|
|
12
|
+
beforeEach(function() {
|
|
13
|
+
const mocks = require('../../test/mocks');
|
|
14
|
+
core = mocks.core();
|
|
15
|
+
core.logger = mocks.logger();
|
|
16
|
+
core.agentify = mocks.agentify();
|
|
17
|
+
core.config = mocks.config();
|
|
18
|
+
core.config.agent.node.enable_rewrite = true;
|
|
19
|
+
core.rewriter = mocks.rewriter();
|
|
20
|
+
origCompileSpy = sinon.spy(Module.prototype, '_compile');
|
|
21
|
+
rewriteHooks = require('./rewrite-hooks')(core);
|
|
22
|
+
rewriteHooks.install();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(function() {
|
|
26
|
+
delete require.cache[testCode];
|
|
27
|
+
rewriteHooks.restore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('Should not rewrite code when enable_rewrite is false', function() {
|
|
31
|
+
sinon.restore();
|
|
32
|
+
core.config.agent.node.enable_rewrite = false;
|
|
33
|
+
rewriteHooks = require('./rewrite-hooks')(core);
|
|
34
|
+
rewriteHooks.install();
|
|
35
|
+
require('../test/resources/file');
|
|
36
|
+
expect(core.rewriter.rewrite).to.not.have.been.called;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('Should rewrite code', function() {
|
|
40
|
+
require('../test/resources/file');
|
|
41
|
+
expect(core.rewriter.rewrite).to.have.been.calledWith(
|
|
42
|
+
sinon.match.string,
|
|
43
|
+
{ filename: testCode }
|
|
44
|
+
);
|
|
45
|
+
expect(core.logger.error).to.not.have.been.called;
|
|
46
|
+
expect(origCompileSpy).to.have.been.calledWith(
|
|
47
|
+
"const ContrastMethods = global.ContrastMethods\n/* eslint-disable */\n'use strict'\nconst variable = 'variable';\n",
|
|
48
|
+
testCode
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('Should log error if module contains a SyntaxError', function() {
|
|
53
|
+
core.rewriter.rewrite.callsFake((content) => ({
|
|
54
|
+
code: content.replace('const variable', 'const variable const a')
|
|
55
|
+
}));
|
|
56
|
+
require('../test/resources/file');
|
|
57
|
+
expect(core.rewriter.rewrite).to.have.been.calledWith(
|
|
58
|
+
sinon.match.string,
|
|
59
|
+
{ filename: testCode }
|
|
60
|
+
);
|
|
61
|
+
expect(core.logger.error).to.have.been.calledWith(
|
|
62
|
+
{
|
|
63
|
+
err: sinon.match({
|
|
64
|
+
message: 'Missing initializer in const declaration'
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
'Failed to compile rewritten code for %s, rewritten code %s, compiling original code.',
|
|
68
|
+
testCode,
|
|
69
|
+
sinon.match('const variable const a')
|
|
70
|
+
);
|
|
71
|
+
expect(origCompileSpy).to.have.been.calledWith(
|
|
72
|
+
"/* eslint-disable */\n'use strict'\nconst variable = 'variable';\n",
|
|
73
|
+
testCode
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
package/lib/sources.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function(core) {
|
|
4
|
+
const { depHooks, patcher, logger, scopes: { sources: sourcesStorage } } = core;
|
|
5
|
+
const sources = core.sources = {};
|
|
6
|
+
|
|
7
|
+
sources.install = function install() {
|
|
8
|
+
depHooks.resolve({ name: 'http' }, http => patchCreateServer({ serverSource: http, patchName: 'httpServer' }));
|
|
9
|
+
depHooks.resolve({ name: 'https' }, https => patchCreateServer({ serverSource: https, patchName: 'httpsServer' }));
|
|
10
|
+
depHooks.resolve({ name: 'http2' }, http2 => {
|
|
11
|
+
patchCreateServer({ serverSource: http2, patchName: 'http2Server' });
|
|
12
|
+
patchCreateServer({ serverSource: http2, patchName: 'http2SecureServer', constructorName: 'createSecureServer' });
|
|
13
|
+
});
|
|
14
|
+
depHooks.resolve({ name: 'spdy' }, spdy => patchCreateServer({ serverSource: spdy, patchName: 'spdyServer' }));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function patchCreateServer({ serverSource, patchName, constructorName = 'createServer' }) {
|
|
18
|
+
patcher.patch(serverSource, constructorName, {
|
|
19
|
+
name: patchName,
|
|
20
|
+
patchType: 'http-sources',
|
|
21
|
+
post: createServerPostHook(patchName)
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createServerPostHook(serverType) {
|
|
26
|
+
return function(data) {
|
|
27
|
+
const { result: server } = data;
|
|
28
|
+
const serverPrototype = server ? Object.getPrototypeOf(server) : null;
|
|
29
|
+
|
|
30
|
+
if (!serverPrototype) {
|
|
31
|
+
logger.error('Unable to patch server prototype, continue without instrumentation');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
patcher.patch(serverPrototype, 'emit', {
|
|
36
|
+
name: 'server.emit',
|
|
37
|
+
patchType: 'req-async-storage',
|
|
38
|
+
around: emitAroundHook(serverType)
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function emitAroundHook(serverType) {
|
|
44
|
+
return function emitAroundHook(next, data) {
|
|
45
|
+
const { args: [event] } = data;
|
|
46
|
+
|
|
47
|
+
if (event !== 'request') return next();
|
|
48
|
+
|
|
49
|
+
const store = { serverType };
|
|
50
|
+
|
|
51
|
+
return sourcesStorage.run(store, next);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
install: sources.install,
|
|
57
|
+
functions: {
|
|
58
|
+
patchCreateServer,
|
|
59
|
+
createServerPostHook,
|
|
60
|
+
emitAroundHook
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const sourcesModule = require('./sources');
|
|
6
|
+
|
|
7
|
+
describe('agentify sources', function () {
|
|
8
|
+
let mocks, coreMock, depHooksMock, patcherMock, loggerMock, scopesMock;
|
|
9
|
+
|
|
10
|
+
beforeEach(function () {
|
|
11
|
+
mocks = require('../../test/mocks');
|
|
12
|
+
loggerMock = mocks.logger();
|
|
13
|
+
patcherMock = mocks.patcher();
|
|
14
|
+
depHooksMock = mocks.depHooks();
|
|
15
|
+
scopesMock = mocks.scopes();
|
|
16
|
+
coreMock = {
|
|
17
|
+
logger: loggerMock,
|
|
18
|
+
patcher: patcherMock,
|
|
19
|
+
depHooks: depHooksMock,
|
|
20
|
+
scopes: scopesMock,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('attaching the sources module to the core object', function () {
|
|
25
|
+
it('attaches the module to the core object', function () {
|
|
26
|
+
const sources = sourcesModule(coreMock);
|
|
27
|
+
expect(coreMock).to.haveOwnProperty('sources');
|
|
28
|
+
expect(coreMock.sources).to.haveOwnProperty('install');
|
|
29
|
+
expect(sources).to.haveOwnProperty('install');
|
|
30
|
+
expect(sources).to.haveOwnProperty('functions');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('installing instrumentation for HTTP type of sources', function () {
|
|
35
|
+
let argumentsMap, httpMock, httpsMock, http2Mock, spdyMock;
|
|
36
|
+
|
|
37
|
+
beforeEach(function () {
|
|
38
|
+
httpMock = { name: 'http' };
|
|
39
|
+
httpsMock = { name: 'https' };
|
|
40
|
+
http2Mock = { name: 'http2' };
|
|
41
|
+
spdyMock = { name: 'spdy' };
|
|
42
|
+
const patchType = 'http-sources';
|
|
43
|
+
argumentsMap = {
|
|
44
|
+
http: {
|
|
45
|
+
source: httpMock,
|
|
46
|
+
patcherArgs: [
|
|
47
|
+
[httpMock, 'createServer', { name: 'httpServer', patchType }],
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
https: {
|
|
51
|
+
source: httpsMock,
|
|
52
|
+
patcherArgs: [
|
|
53
|
+
[httpsMock, 'createServer', { name: 'httpsServer', patchType }],
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
http2: {
|
|
57
|
+
source: http2Mock,
|
|
58
|
+
patcherArgs: [
|
|
59
|
+
[http2Mock, 'createServer', { name: 'http2Server', patchType }],
|
|
60
|
+
[
|
|
61
|
+
http2Mock,
|
|
62
|
+
'createSecureServer',
|
|
63
|
+
{ name: 'http2SecureServer', patchType },
|
|
64
|
+
],
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
spdy: {
|
|
68
|
+
source: spdyMock,
|
|
69
|
+
patcherArgs: [
|
|
70
|
+
[spdyMock, 'createServer', { name: 'spdyServer', patchType }],
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
coreMock.depHooks.resolve.callsFake((moduleMetadata, callback) =>
|
|
75
|
+
callback(argumentsMap[moduleMetadata.name].source)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should call patcher.patch with the correct arguments from depHooks.resolve', function () {
|
|
80
|
+
const sources = sourcesModule(coreMock);
|
|
81
|
+
sources.install();
|
|
82
|
+
|
|
83
|
+
const callOrder = ['http', 'https', 'http2', 'http2', 'spdy'];
|
|
84
|
+
let consecutiveCalls = 0;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < callOrder.length; i++) {
|
|
87
|
+
consecutiveCalls = callOrder[i] == 'http2' ? consecutiveCalls : 0;
|
|
88
|
+
const argument = (callNumber, argIndex) =>
|
|
89
|
+
patcherMock.patch.getCall(callNumber).args[argIndex];
|
|
90
|
+
const expectedArgument = (callNumber, argIndex, consecutiveCalls) =>
|
|
91
|
+
argumentsMap[callOrder[callNumber]].patcherArgs[consecutiveCalls][
|
|
92
|
+
argIndex
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
expect(argument(i, 0)).to.equal(
|
|
96
|
+
expectedArgument(i, 0, consecutiveCalls)
|
|
97
|
+
);
|
|
98
|
+
expect(argument(i, 1)).to.equal(
|
|
99
|
+
expectedArgument(i, 1, consecutiveCalls)
|
|
100
|
+
);
|
|
101
|
+
expect(argument(i, 2)).to.include(
|
|
102
|
+
expectedArgument(i, 2, consecutiveCalls)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (callOrder[i] == 'http2' && !consecutiveCalls) {
|
|
106
|
+
consecutiveCalls++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
expect(patcherMock.patch).to.have.been.callCount(callOrder.length);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('functions', function () {
|
|
115
|
+
let emitAroundHook, createServerPostHook, dataObj;
|
|
116
|
+
|
|
117
|
+
beforeEach(function () {
|
|
118
|
+
({
|
|
119
|
+
functions: { createServerPostHook, emitAroundHook },
|
|
120
|
+
} = sourcesModule(coreMock));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('createServerPostHook', function () {
|
|
124
|
+
let serverMock, prototypeMock;
|
|
125
|
+
beforeEach(function () {
|
|
126
|
+
serverMock = {};
|
|
127
|
+
prototypeMock = {
|
|
128
|
+
emit() {},
|
|
129
|
+
};
|
|
130
|
+
dataObj = {
|
|
131
|
+
result: serverMock,
|
|
132
|
+
};
|
|
133
|
+
Object.setPrototypeOf(serverMock, prototypeMock);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('calls patcher.patch() if proper data argument is supplied', function () {
|
|
137
|
+
createServerPostHook('httpServer')(dataObj);
|
|
138
|
+
expect(loggerMock.error).to.not.have.been.called;
|
|
139
|
+
expect(patcherMock.patch).to.have.been.calledWith(
|
|
140
|
+
prototypeMock,
|
|
141
|
+
'emit',
|
|
142
|
+
{
|
|
143
|
+
name: 'server.emit',
|
|
144
|
+
patchType: 'req-async-storage',
|
|
145
|
+
around: sinon.match.func
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('calls logger.error() if none or invalid data argument is supplied', function () {
|
|
151
|
+
createServerPostHook('httpServer')({});
|
|
152
|
+
expect(loggerMock.error).to.have.been.calledWith(
|
|
153
|
+
'Unable to patch server prototype, continue without instrumentation'
|
|
154
|
+
);
|
|
155
|
+
Object.setPrototypeOf(serverMock, null);
|
|
156
|
+
createServerPostHook('httpServer')(serverMock);
|
|
157
|
+
expect(loggerMock.error).to.have.been.called.calledTwice;
|
|
158
|
+
expect(patcherMock.patch).to.not.have.been.called;
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('emitAroundHook', function () {
|
|
163
|
+
let nextFn, reqMock, resMock, eventMock;
|
|
164
|
+
beforeEach(function () {
|
|
165
|
+
reqMock = () => {
|
|
166
|
+
this.method = 'GET';
|
|
167
|
+
this.url = 'http://localhost';
|
|
168
|
+
};
|
|
169
|
+
resMock = () => {};
|
|
170
|
+
eventMock = 'request';
|
|
171
|
+
dataObj = {
|
|
172
|
+
args: [eventMock, reqMock, resMock],
|
|
173
|
+
};
|
|
174
|
+
nextFn = function () {
|
|
175
|
+
return {
|
|
176
|
+
sourcesStore: scopesMock.sources.getStore(),
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('executes the hooked nextFn in the provided context if the event name is "request"', function () {
|
|
182
|
+
const result = emitAroundHook('httpServer')(nextFn, dataObj);
|
|
183
|
+
expect(result.sourcesStore).to.deep.include({
|
|
184
|
+
serverType: 'httpServer',
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('executes the hooked nextFn without binding it to a context if the event name is not "request"', function () {
|
|
189
|
+
eventMock = 'listen';
|
|
190
|
+
dataObj.args[0] = eventMock;
|
|
191
|
+
const result = emitAroundHook('httpServer')(nextFn, dataObj);
|
|
192
|
+
expect(result.sourcesStore).to.equal(undefined);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contrast/agentify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Configures Contrast agent services and instrumentation within an application",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"types": "lib/index.d.ts",
|
|
12
|
+
"engines": {
|
|
13
|
+
"npm": ">= 8.4.0",
|
|
14
|
+
"node": ">= 14.15.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "../scripts/test.sh"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {}
|
|
20
|
+
}
|