@contrast/core 1.32.3 → 1.34.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/agent-info.js +7 -1
- package/lib/app-info.js +1 -1
- package/lib/app-info.test.js +82 -0
- package/lib/capture-stacktrace.test.js +55 -0
- package/lib/contrast-methods.test.js +299 -0
- package/lib/index.d.ts +3 -2
- package/lib/sensitive-data-masking/index.test.js +163 -0
- package/lib/system-info/cloud-provider-metadata.js +146 -0
- package/lib/system-info/cloud-provider-metadata.test.js +160 -0
- package/lib/system-info/index.js +55 -40
- package/lib/system-info/index.test.js +34 -0
- package/package.json +4 -3
package/lib/agent-info.js
CHANGED
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { randomUUID } = require('crypto');
|
|
18
19
|
const { name: agentName, version: agentVersion } = require('../package.json');
|
|
19
20
|
|
|
20
21
|
module.exports = function init(core) {
|
|
21
|
-
// default to version of core
|
|
22
|
+
// default to name and version of core
|
|
22
23
|
if (!core.agentName) {
|
|
23
24
|
core.agentName = agentName;
|
|
24
25
|
}
|
|
@@ -26,5 +27,10 @@ module.exports = function init(core) {
|
|
|
26
27
|
core.agentVersion = agentVersion;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// default to a new random UUID
|
|
31
|
+
if (!core.reportingInstance) {
|
|
32
|
+
core.reportingInstance = randomUUID();
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
return core;
|
|
30
36
|
};
|
package/lib/app-info.js
CHANGED
|
@@ -167,7 +167,7 @@ module.exports = function (core) {
|
|
|
167
167
|
|
|
168
168
|
for (dir of dirs) {
|
|
169
169
|
try {
|
|
170
|
-
packageFile = findPackageJsonSync({ cwd: dir });
|
|
170
|
+
packageFile = process.env.npm_package_json ?? findPackageJsonSync({ cwd: dir });
|
|
171
171
|
packageData = require(packageFile);
|
|
172
172
|
break;
|
|
173
173
|
} catch (err) {} // eslint-disable-line no-empty
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const proxyquire = require('proxyquire');
|
|
6
|
+
const mocks = require('@contrast/test/mocks');
|
|
7
|
+
|
|
8
|
+
describe('core app-info', function () {
|
|
9
|
+
let os, core, appInfo;
|
|
10
|
+
|
|
11
|
+
beforeEach(function () {
|
|
12
|
+
core = mocks.core();
|
|
13
|
+
core.config = mocks.config();
|
|
14
|
+
core.config.server.type = 'Node.js';
|
|
15
|
+
core.config.server.environment = 'PRODUCTION';
|
|
16
|
+
core.logger = mocks.logger();
|
|
17
|
+
core.config.server.name = 'zoing';
|
|
18
|
+
|
|
19
|
+
os = {
|
|
20
|
+
type: sinon.stub().returns('Linux'),
|
|
21
|
+
platform: sinon.stub().returns('linux'),
|
|
22
|
+
arch: sinon.stub().returns('x64'),
|
|
23
|
+
release: sinon.stub().returns('1.23.45'),
|
|
24
|
+
hostname: sinon.stub().returns('hostname'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
appInfo = proxyquire(
|
|
28
|
+
'./app-info',
|
|
29
|
+
{
|
|
30
|
+
os,
|
|
31
|
+
process: {
|
|
32
|
+
...process,
|
|
33
|
+
argv: ['node', __filename],
|
|
34
|
+
version: 'v14.17.5'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)(core);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('builds out app data from os and process information', function () {
|
|
41
|
+
expect(appInfo).to.deep.include({
|
|
42
|
+
os: {
|
|
43
|
+
type: 'Linux',
|
|
44
|
+
platform: 'linux',
|
|
45
|
+
architecture: 'x64',
|
|
46
|
+
release: '1.23.45'
|
|
47
|
+
},
|
|
48
|
+
hostname: 'hostname',
|
|
49
|
+
indexFile: __filename,
|
|
50
|
+
serverVersion: undefined,
|
|
51
|
+
node_version: 'v14.17.5',
|
|
52
|
+
appPath: '/',
|
|
53
|
+
serverName: 'zoing',
|
|
54
|
+
serverType: 'Node.js',
|
|
55
|
+
serverEnvironment: 'PRODUCTION',
|
|
56
|
+
group: undefined,
|
|
57
|
+
metadata: undefined
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("instantiates with _errors if package con't be found at provided app_root", function() {
|
|
62
|
+
const { npm_package_json } = process.env;
|
|
63
|
+
delete process.env.npm_package_json;
|
|
64
|
+
|
|
65
|
+
core.config.agent.node.app_root = '/no/package/at/this/path';
|
|
66
|
+
appInfo = proxyquire(
|
|
67
|
+
'./app-info',
|
|
68
|
+
{
|
|
69
|
+
os,
|
|
70
|
+
process: {
|
|
71
|
+
...process,
|
|
72
|
+
argv: ['node', ''],
|
|
73
|
+
version: 'v14.17.5'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
const fn = () => appInfo(core);
|
|
78
|
+
expect(fn).throws('unable to locate application package.json');
|
|
79
|
+
|
|
80
|
+
process.env.npm_package_json = npm_package_json;
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const mocks = require('@contrast/test/mocks');
|
|
5
|
+
|
|
6
|
+
describe('core capture-stacktrace', function g() {
|
|
7
|
+
let core;
|
|
8
|
+
|
|
9
|
+
beforeEach(function () {
|
|
10
|
+
core = mocks.core();
|
|
11
|
+
core.config = mocks.config();
|
|
12
|
+
require('./capture-stacktrace')(core);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('appends a stack getter property', function () {
|
|
16
|
+
const obj = { foo: 'bar' };
|
|
17
|
+
core.captureStacktrace(obj);
|
|
18
|
+
const desc = Object.getOwnPropertyDescriptor(obj, 'stack');
|
|
19
|
+
expect(typeof desc.get).to.equal('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('generates a stacktrace from given options', function () {
|
|
23
|
+
(function outer() {
|
|
24
|
+
(function inner() {
|
|
25
|
+
const obj = {};
|
|
26
|
+
core.captureStacktrace(obj, { constructorOpt: inner });
|
|
27
|
+
expect(obj.stack[0]).to.have.property('method', 'outer');
|
|
28
|
+
expect(obj.stack).to.have.lengthOf(10);
|
|
29
|
+
})();
|
|
30
|
+
})();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('createSnapshot', function () {
|
|
34
|
+
it('prepends desired frames', function() {
|
|
35
|
+
const frame = { foo: 'bar' };
|
|
36
|
+
const stub = function stub() { };
|
|
37
|
+
|
|
38
|
+
const snapshot = core.createSnapshot({ prependFrames: [frame, stub] });
|
|
39
|
+
const result = snapshot();
|
|
40
|
+
expect(result[0]).to.equal(frame);
|
|
41
|
+
expect(result[1]).to.have.property('method', 'stub');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('handles `eval`', function () {
|
|
45
|
+
const snapshot = eval('core.createSnapshot()');
|
|
46
|
+
const result = snapshot();
|
|
47
|
+
|
|
48
|
+
expect(result).to.have.length(10);
|
|
49
|
+
const evalFrame = result[3];
|
|
50
|
+
expect(evalFrame).to.have.property('eval').that.is.not.undefined;
|
|
51
|
+
expect(evalFrame).to.have.property('file').that.matches(/capture-stacktrace.test.js$/);
|
|
52
|
+
expect(evalFrame).to.have.property('method', 'eval');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const mocks = require('@contrast/test/mocks');
|
|
6
|
+
const patcher = require('@contrast/patcher');
|
|
7
|
+
|
|
8
|
+
const { stringify } = JSON;
|
|
9
|
+
|
|
10
|
+
const title = (method, args, expected) =>
|
|
11
|
+
`${method}(${args.map(stringify).join(', ')}) = ${stringify(expected)}`;
|
|
12
|
+
|
|
13
|
+
describe('core contrast-methods', function () {
|
|
14
|
+
let core, contrastMethods, api;
|
|
15
|
+
|
|
16
|
+
beforeEach(function () {
|
|
17
|
+
core = mocks.core();
|
|
18
|
+
core.logger = mocks.logger();
|
|
19
|
+
core.patcher = patcher(core);
|
|
20
|
+
sinon.spy(core.patcher, 'patch');
|
|
21
|
+
contrastMethods = require('./contrast-methods')(core);
|
|
22
|
+
api = contrastMethods.api;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const GENERIC_TESTS = [{}, null, 1, 'two', undefined];
|
|
26
|
+
|
|
27
|
+
describe('.api', function () {
|
|
28
|
+
describe('.eval()', function () {
|
|
29
|
+
GENERIC_TESTS.forEach((arg) => {
|
|
30
|
+
it(title('eval', [arg], arg), function () {
|
|
31
|
+
expect(api.eval(arg)).to.equal(arg);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const ADD_TESTS = [
|
|
37
|
+
[[1, 2], 3],
|
|
38
|
+
[[1, '2'], '12'],
|
|
39
|
+
[['foo', 'bar'], 'foobar'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
describe('.addAssign()', function () {
|
|
43
|
+
ADD_TESTS.forEach(([args, expected]) => {
|
|
44
|
+
it(title('addAssign', args, expected), function () {
|
|
45
|
+
expect(api.addAssign(...args)).to.equal(expected);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('.add()', function () {
|
|
51
|
+
ADD_TESTS.forEach(([args, expected], i) => {
|
|
52
|
+
it(title('add', args, expected), function () {
|
|
53
|
+
expect(api.add(...args)).to.equal(expected);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const obj1 = { foo: 'bar' };
|
|
59
|
+
const obj2 = { foo: 'bar' };
|
|
60
|
+
const EQ_TESTS = [
|
|
61
|
+
[['', ''], true, true],
|
|
62
|
+
[['foo', 'foo'], true, true],
|
|
63
|
+
[['foo', 'bar'], false, false],
|
|
64
|
+
[[1, 1], true, true],
|
|
65
|
+
[[1, 2], false, false],
|
|
66
|
+
[[1, '1'], true, false],
|
|
67
|
+
[[obj1, obj1], true, true],
|
|
68
|
+
[[obj1, obj2], false, false],
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
describe('.eqEq()', function () {
|
|
72
|
+
EQ_TESTS.forEach(([args, expected]) => {
|
|
73
|
+
it(title('eqEq', args, expected), function () {
|
|
74
|
+
expect(api.eqEq(...args)).to.equal(expected);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('.eqEqEq()', function () {
|
|
80
|
+
EQ_TESTS.forEach(([args, _, expected]) => {
|
|
81
|
+
it(title('eqEqEq', args, expected), function () {
|
|
82
|
+
expect(api.eqEqEq(...args)).to.equal(expected);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('.notEq()', function () {
|
|
88
|
+
EQ_TESTS.forEach(([args, expected]) => {
|
|
89
|
+
it(title('notEq', args, !expected), function () {
|
|
90
|
+
expect(api.notEq(...args)).to.equal(!expected);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('.notEqEq()', function () {
|
|
96
|
+
EQ_TESTS.forEach(([args, _, expected]) => {
|
|
97
|
+
it(title('notEqEq', args, !expected), function () {
|
|
98
|
+
expect(api.notEqEq(...args)).to.equal(!expected);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('.forceCopy()', function () {
|
|
104
|
+
GENERIC_TESTS.forEach((arg) => {
|
|
105
|
+
it(title('forceCopy', [arg], arg), function () {
|
|
106
|
+
expect(api.forceCopy(arg)).to.equal(arg);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('.cast()', function () {
|
|
112
|
+
GENERIC_TESTS.forEach((arg) => {
|
|
113
|
+
it(title('cast', [arg], arg), function () {
|
|
114
|
+
expect(api.cast(arg)).to.equal(arg);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('.tag()', function () {
|
|
120
|
+
const bar = 'bar';
|
|
121
|
+
const baz = 'baz';
|
|
122
|
+
|
|
123
|
+
[
|
|
124
|
+
[[['']], ``], // eslint-disable-line quotes
|
|
125
|
+
[[['foo']], `foo`], // eslint-disable-line quotes
|
|
126
|
+
[[['', ''], bar], `${bar}`],
|
|
127
|
+
[[['foo', ''], bar], `foo${bar}`],
|
|
128
|
+
[[['foo', 'boo'], bar], `foo${bar}boo`],
|
|
129
|
+
[[['foo', 'boo', ''], bar, baz], `foo${bar}boo${baz}`],
|
|
130
|
+
[[['foo', 'boo', '.'], bar, baz], `foo${bar}boo${baz}.`],
|
|
131
|
+
].forEach(([args, template]) => {
|
|
132
|
+
it(title('tag', args, template), function () {
|
|
133
|
+
expect(api.tag(...args)).to.equal(template);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('.Function()', function () {
|
|
139
|
+
it('patches the Function global', function () {
|
|
140
|
+
expect(core.patcher.patch).to.have.been.calledWithExactly(Function, {
|
|
141
|
+
name: 'global.Function',
|
|
142
|
+
patchType: 'rewrite-injection',
|
|
143
|
+
funcKey: 'rewrite-injection:global.Function',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('acts as a function constructor', function () {
|
|
148
|
+
const fn = api.Function('return "hello world"');
|
|
149
|
+
|
|
150
|
+
expect(fn()).to.equal('hello world');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('.Number()', function () {
|
|
155
|
+
it('patches the Number global', function () {
|
|
156
|
+
expect(core.patcher.patch).to.have.been.calledWithExactly(Number, {
|
|
157
|
+
name: 'global.Number',
|
|
158
|
+
patchType: 'rewrite-injection',
|
|
159
|
+
funcKey: 'rewrite-injection:global.Number',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('Number(14)', function () {
|
|
164
|
+
const result = api.Number(14);
|
|
165
|
+
|
|
166
|
+
expect(typeof result).to.equal('number');
|
|
167
|
+
expect(result).to.equal(14);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("Number('14')", function () {
|
|
171
|
+
const result = api.Number('14');
|
|
172
|
+
|
|
173
|
+
expect(typeof result).to.equal('number');
|
|
174
|
+
expect(result).to.equal(14);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("Number('boo')", function () {
|
|
178
|
+
const result = api.Number('boo');
|
|
179
|
+
|
|
180
|
+
expect(typeof result).to.equal('number');
|
|
181
|
+
expect(result).to.be.NaN;
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('.Object()', function () {
|
|
186
|
+
it('patches the Object global', function () {
|
|
187
|
+
expect(core.patcher.patch).to.have.been.calledWithExactly(Object, {
|
|
188
|
+
name: 'global.Object',
|
|
189
|
+
patchType: 'rewrite-injection',
|
|
190
|
+
funcKey: 'rewrite-injection:global.Object',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('Object(null)', function () {
|
|
195
|
+
const result = api.Object(null);
|
|
196
|
+
|
|
197
|
+
expect(typeof result).to.equal('object');
|
|
198
|
+
expect(result).not.to.equal({});
|
|
199
|
+
expect(result).to.deep.equal({});
|
|
200
|
+
expect(result.toString()).to.equal('[object Object]');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("Object({ foo: 'bar' })", function () {
|
|
204
|
+
const obj = { foo: 'bar' };
|
|
205
|
+
const result = api.Object(obj);
|
|
206
|
+
|
|
207
|
+
expect(typeof result).to.equal('object');
|
|
208
|
+
expect(result).to.equal(obj);
|
|
209
|
+
expect(result.toString()).to.equal('[object Object]');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("Object('boo')", function () {
|
|
213
|
+
const str = 'boo';
|
|
214
|
+
const result = api.Object(str);
|
|
215
|
+
|
|
216
|
+
expect(typeof result).to.equal('object');
|
|
217
|
+
expect(result == str).to.be.true;
|
|
218
|
+
expect(result === str).to.be.false;
|
|
219
|
+
expect(result.toString()).to.equal(str);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('.String()', function () {
|
|
224
|
+
it('patches the String global', function () {
|
|
225
|
+
expect(core.patcher.patch).to.have.been.calledWithExactly(String, {
|
|
226
|
+
name: 'global.String',
|
|
227
|
+
patchType: 'rewrite-injection',
|
|
228
|
+
funcKey: 'rewrite-injection:global.String',
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("String('boo')", function () {
|
|
233
|
+
const str = 'boo';
|
|
234
|
+
const result = api.String(str);
|
|
235
|
+
|
|
236
|
+
expect(typeof result).to.equal('string');
|
|
237
|
+
expect(result).to.equal(str);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('String(14)', function () {
|
|
241
|
+
const result = api.String(14);
|
|
242
|
+
|
|
243
|
+
expect(typeof result).to.equal('string');
|
|
244
|
+
expect(result).to.equal('14');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("String({ foo: 'bar' })", function () {
|
|
248
|
+
const result = api.String({ foo: 'bar' });
|
|
249
|
+
|
|
250
|
+
expect(typeof result).to.equal('string');
|
|
251
|
+
expect(result).to.equal('[object Object]');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('.getGlobal()', function () {
|
|
257
|
+
it('returns global', function () {
|
|
258
|
+
expect(contrastMethods.getGlobal()).to.equal(global);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('.install()', function () {
|
|
263
|
+
let globalMock;
|
|
264
|
+
|
|
265
|
+
beforeEach(function () {
|
|
266
|
+
globalMock = {};
|
|
267
|
+
sinon.stub(contrastMethods, 'getGlobal').returns(globalMock);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('installs on global a single time', function () {
|
|
271
|
+
expect(globalMock).not.to.have.property('ContrastMethods');
|
|
272
|
+
|
|
273
|
+
contrastMethods.install();
|
|
274
|
+
expect(globalMock).to.have.property('ContrastMethods', api);
|
|
275
|
+
expect(contrastMethods.getGlobal).to.have.been.calledOnce;
|
|
276
|
+
|
|
277
|
+
expect(() => {
|
|
278
|
+
delete globalMock.ContrastMethods;
|
|
279
|
+
}).throw("Cannot delete property 'ContrastMethods'");
|
|
280
|
+
|
|
281
|
+
contrastMethods.install();
|
|
282
|
+
expect(globalMock).to.have.property('ContrastMethods', api);
|
|
283
|
+
expect(contrastMethods.getGlobal).to.have.been.calledOnce;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('logs an error when global.ContrastMethods cannot be injected', function () {
|
|
287
|
+
Object.defineProperty(globalMock, 'ContrastMethods', {
|
|
288
|
+
configurable: false,
|
|
289
|
+
value: 'does not matter for test',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
contrastMethods.install();
|
|
293
|
+
expect(core.logger.error).to.have.been.calledWith(
|
|
294
|
+
sinon.match(Error),
|
|
295
|
+
'Unable to define global.ContrastMethods',
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { AppInfo, Messages } from '@contrast/common';
|
|
16
|
+
import { AppInfo, Messages, SystemInfo } from '@contrast/common';
|
|
17
17
|
|
|
18
18
|
interface Frame {
|
|
19
19
|
eval: string | undefined;
|
|
@@ -32,6 +32,7 @@ interface CreateSnapshotOpts {
|
|
|
32
32
|
export interface Core {
|
|
33
33
|
agentName: string;
|
|
34
34
|
agentVersion: string;
|
|
35
|
+
reportingInstance: string;
|
|
35
36
|
|
|
36
37
|
appInfo: AppInfo;
|
|
37
38
|
|
|
@@ -44,5 +45,5 @@ export interface Core {
|
|
|
44
45
|
|
|
45
46
|
sensitiveDataMasking: any;
|
|
46
47
|
|
|
47
|
-
getSystemInfo():
|
|
48
|
+
getSystemInfo(): Promise<SystemInfo>;
|
|
48
49
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe } = require('mocha');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const dataMaskingFactory = require('../sensitive-data-masking');
|
|
6
|
+
const mocks = require('@contrast/test/mocks');
|
|
7
|
+
const { Event, Rule } = require('@contrast/common');
|
|
8
|
+
|
|
9
|
+
describe('core sensitive-data-masking', function () {
|
|
10
|
+
let core, sdMasking, reqData, parsedParams, parsedCookies, parsedQuery, parsedBody, protect, resultsMap, trackRequest;
|
|
11
|
+
|
|
12
|
+
const ssn = '123-12-1234';
|
|
13
|
+
const cvv = '123';
|
|
14
|
+
const routingNumber = '123123123';
|
|
15
|
+
const apiToken = '$123-abcd<456*def';
|
|
16
|
+
const policy = require('../../test/resources/sensitive-data-policy');
|
|
17
|
+
|
|
18
|
+
beforeEach(function () {
|
|
19
|
+
core = mocks.core();
|
|
20
|
+
core.config = mocks.config();
|
|
21
|
+
core.logger = mocks.logger();
|
|
22
|
+
core.protect = mocks.protect();
|
|
23
|
+
core.config.api.enable = false;
|
|
24
|
+
|
|
25
|
+
reqData = {
|
|
26
|
+
uriPath: `/test/${cvv}/stuff`,
|
|
27
|
+
queries: `apiToken=${apiToken}&foo=bar`,
|
|
28
|
+
headers: [
|
|
29
|
+
'routingAccNum',
|
|
30
|
+
routingNumber,
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
parsedQuery = { ssn };
|
|
34
|
+
parsedParams = { cvv };
|
|
35
|
+
parsedCookies = { ssn };
|
|
36
|
+
parsedBody = { ssn, input: '../../etc/passwd' };
|
|
37
|
+
|
|
38
|
+
trackRequest = true;
|
|
39
|
+
resultsMap = {
|
|
40
|
+
'sql-injection': [
|
|
41
|
+
{
|
|
42
|
+
value: apiToken,
|
|
43
|
+
score: 10,
|
|
44
|
+
ruleId: Rule.SQL_INJECTION,
|
|
45
|
+
path: [],
|
|
46
|
+
mappedId: 'sql-injection',
|
|
47
|
+
key: 'apiToken',
|
|
48
|
+
inputType: 'HeaderValue',
|
|
49
|
+
idsList: [
|
|
50
|
+
'worth-watching'
|
|
51
|
+
],
|
|
52
|
+
exploitMetadata: [],
|
|
53
|
+
blocked: false
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
value: ssn,
|
|
57
|
+
score: 10,
|
|
58
|
+
ruleId: Rule.SQL_INJECTION,
|
|
59
|
+
path: [],
|
|
60
|
+
mappedId: 'sql-injection',
|
|
61
|
+
key: 'ssn',
|
|
62
|
+
inputType: 'JsonValue',
|
|
63
|
+
idsList: [],
|
|
64
|
+
exploitMetadata: [],
|
|
65
|
+
blocked: false
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
protect = {
|
|
71
|
+
reqData,
|
|
72
|
+
parsedQuery,
|
|
73
|
+
parsedParams,
|
|
74
|
+
parsedCookies,
|
|
75
|
+
parsedBody,
|
|
76
|
+
resultsMap,
|
|
77
|
+
trackRequest
|
|
78
|
+
};
|
|
79
|
+
sdMasking = dataMaskingFactory(core);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('logs when no policy is in server-settings message', function () {
|
|
83
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {});
|
|
84
|
+
expect(sdMasking.policy.keywordSets).to.be.empty;
|
|
85
|
+
expect(sdMasking.policy.idMap).to.be.empty;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handler is noop if no settings have been published', function () {
|
|
89
|
+
const store = { protect };
|
|
90
|
+
|
|
91
|
+
core.messages.emit(Event.PROTECT, store);
|
|
92
|
+
|
|
93
|
+
expect(reqData.uriPath).to.contain(cvv);
|
|
94
|
+
expect(reqData.queries).to.contain(apiToken);
|
|
95
|
+
expect(reqData.headers[1]).to.equal(routingNumber);
|
|
96
|
+
expect(parsedCookies).to.have.property('ssn', ssn);
|
|
97
|
+
expect(parsedQuery).to.have.property('ssn', ssn);
|
|
98
|
+
expect(parsedParams).to.have.property('cvv', cvv);
|
|
99
|
+
expect(protect.parsedBody).to.have.property('ssn', ssn);
|
|
100
|
+
expect(
|
|
101
|
+
resultsMap['sql-injection'][0]
|
|
102
|
+
).to.have.property('value', apiToken);
|
|
103
|
+
expect(
|
|
104
|
+
store.protect.resultsMap['sql-injection'][1]
|
|
105
|
+
).to.have.property('value', ssn);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles protect message', function () {
|
|
109
|
+
const store = { protect };
|
|
110
|
+
|
|
111
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, { sensitive_data_masking_policy: policy });
|
|
112
|
+
expect(core.logger.trace).to.have.been.calledWith('updating sensitive data masking policy');
|
|
113
|
+
expect(parsedCookies).to.have.property('ssn', ssn);
|
|
114
|
+
|
|
115
|
+
core.messages.emit(Event.PROTECT, store);
|
|
116
|
+
|
|
117
|
+
expect(reqData.uriPath).to.equal('/test/contrast-redacted-financial-info/stuff');
|
|
118
|
+
expect(reqData.queries).to.equal('apiToken=contrast-redacted-authentication-info&foo=bar');
|
|
119
|
+
expect(reqData.headers[1]).to.equal('contrast-redacted-financial-info');
|
|
120
|
+
expect(parsedCookies).to.have.property('ssn', 'contrast-redacted-government-id');
|
|
121
|
+
expect(parsedQuery).to.have.property('ssn', 'contrast-redacted-government-id');
|
|
122
|
+
expect(parsedParams).to.have.property('cvv', 'contrast-redacted-financial-info');
|
|
123
|
+
expect(protect.parsedBody).to.equal('contrast-redacted-body');
|
|
124
|
+
expect(
|
|
125
|
+
resultsMap['sql-injection'][0]
|
|
126
|
+
).to.have.property('value', 'contrast-redacted-authentication-info');
|
|
127
|
+
expect(
|
|
128
|
+
store.protect.resultsMap['sql-injection'][1]
|
|
129
|
+
).to.have.property('value', 'contrast-redacted-government-id');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('policy controls masking of attack vectors and body', function () {
|
|
133
|
+
const store = { protect };
|
|
134
|
+
core.messages.emit(Event.SERVER_SETTINGS_UPDATE, {
|
|
135
|
+
sensitive_data_masking_policy: {
|
|
136
|
+
...policy,
|
|
137
|
+
mask_http_body: false,
|
|
138
|
+
mask_attack_vector: false,
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
expect(core.logger.trace).to.have.been.calledWith('updating sensitive data masking policy');
|
|
142
|
+
expect(parsedCookies).to.have.property('ssn', ssn);
|
|
143
|
+
|
|
144
|
+
core.messages.emit(Event.PROTECT, store);
|
|
145
|
+
|
|
146
|
+
expect(reqData.uriPath).to.equal('/test/contrast-redacted-financial-info/stuff');
|
|
147
|
+
expect(reqData.queries).to.equal('apiToken=contrast-redacted-authentication-info&foo=bar');
|
|
148
|
+
expect(reqData.headers[1]).to.equal('contrast-redacted-financial-info');
|
|
149
|
+
expect(parsedCookies).to.have.property('ssn', 'contrast-redacted-government-id');
|
|
150
|
+
expect(parsedQuery).to.have.property('ssn', 'contrast-redacted-government-id');
|
|
151
|
+
expect(parsedParams).to.have.property('cvv', 'contrast-redacted-financial-info');
|
|
152
|
+
expect(protect.parsedBody).to.deep.equal({
|
|
153
|
+
ssn: 'contrast-redacted-government-id',
|
|
154
|
+
input: '../../etc/passwd'
|
|
155
|
+
});
|
|
156
|
+
expect(
|
|
157
|
+
resultsMap['sql-injection'][0]
|
|
158
|
+
).to.have.property('value', apiToken);
|
|
159
|
+
expect(
|
|
160
|
+
store.protect.resultsMap['sql-injection'][1]
|
|
161
|
+
).to.have.property('value', ssn);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
// @ts-check
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { default: axios } = require('axios');
|
|
19
|
+
const METADATA_ENDPOINT_ADDRESS = 'http://169.254.169.254';
|
|
20
|
+
|
|
21
|
+
/** @param {number} ms */
|
|
22
|
+
const abort = (ms) => {
|
|
23
|
+
const abortController = new AbortController();
|
|
24
|
+
setTimeout(() => abortController.abort(), ms);
|
|
25
|
+
return abortController.signal;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** @param {number} ms */
|
|
29
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
30
|
+
|
|
31
|
+
const FETCHERS = {
|
|
32
|
+
/** @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html */
|
|
33
|
+
async AWS() {
|
|
34
|
+
try {
|
|
35
|
+
const { data: token } = await axios({
|
|
36
|
+
method: 'PUT',
|
|
37
|
+
url: new URL('/latest/api/token', METADATA_ENDPOINT_ADDRESS).href,
|
|
38
|
+
headers: {
|
|
39
|
+
'X-aws-ec2-metadata-token-ttl-seconds': '300'
|
|
40
|
+
},
|
|
41
|
+
proxy: false, // proxies should not be used in any cloud provider.
|
|
42
|
+
signal: abort(5000),
|
|
43
|
+
});
|
|
44
|
+
const { data: document } = await axios({
|
|
45
|
+
method: 'GET',
|
|
46
|
+
url: new URL('/latest/dynamic/instance-identity/document', METADATA_ENDPOINT_ADDRESS).href,
|
|
47
|
+
headers: {
|
|
48
|
+
'X-aws-ec2-metadata-token': token
|
|
49
|
+
},
|
|
50
|
+
proxy: false,
|
|
51
|
+
signal: abort(5000),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (document) {
|
|
55
|
+
const { region, accountId, instanceId } = document;
|
|
56
|
+
return {
|
|
57
|
+
provider: 'aws',
|
|
58
|
+
id: `arn:aws:ec2:${region}:${accountId}:instance/${instanceId}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore, return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
/** @see https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux */
|
|
68
|
+
async Azure() {
|
|
69
|
+
try {
|
|
70
|
+
const { data: resourceId } = await axios({
|
|
71
|
+
method: 'GET',
|
|
72
|
+
url: new URL('/metadata/instance/compute/resourceId?api-version=2021-02-01&format=text', METADATA_ENDPOINT_ADDRESS).href,
|
|
73
|
+
headers: {
|
|
74
|
+
Metadata: 'true'
|
|
75
|
+
},
|
|
76
|
+
proxy: false,
|
|
77
|
+
signal: abort(5000),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (resourceId) {
|
|
81
|
+
return {
|
|
82
|
+
provider: 'azure',
|
|
83
|
+
id: resourceId,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore, return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null;
|
|
91
|
+
},
|
|
92
|
+
/** @see https://cloud.google.com/compute/docs/metadata/querying-metadata */
|
|
93
|
+
async GCP() {
|
|
94
|
+
try {
|
|
95
|
+
const { data: id } = await axios({
|
|
96
|
+
method: 'GET',
|
|
97
|
+
url: new URL('/computeMetadata/v1/instance/id?alt=text', METADATA_ENDPOINT_ADDRESS).href,
|
|
98
|
+
headers: {
|
|
99
|
+
'Metadata-Flavor': 'Google'
|
|
100
|
+
},
|
|
101
|
+
// id is a numerical value too big to handle as a js `number`, so we
|
|
102
|
+
// need to make sure we don't try to parse the response value.
|
|
103
|
+
transformResponse: (res) => res,
|
|
104
|
+
proxy: false,
|
|
105
|
+
signal: abort(5000)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (id) {
|
|
109
|
+
return { provider: 'gcp', id };
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// retry after 1 second on 503
|
|
113
|
+
if (err?.response?.status === 503) {
|
|
114
|
+
await delay(1000);
|
|
115
|
+
return this.GCP();
|
|
116
|
+
}
|
|
117
|
+
// otherwise ignore, return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
/**
|
|
126
|
+
* If passed a `provider`, set by the `inventory.gather_metadata_via` config
|
|
127
|
+
* option, we will only try to retrieve metadata from that cloud provider. If
|
|
128
|
+
* no provider is passed, we will attempt all endpoints in parallel, returning
|
|
129
|
+
* the first result or `null` if none resolve within 5 seconds.
|
|
130
|
+
*
|
|
131
|
+
* @param {import('@contrast/config').Config['inventory']['gather_metadata_via']} provider
|
|
132
|
+
* @returns {Promise<{ provider: string, id: string } | null>}
|
|
133
|
+
*/
|
|
134
|
+
async getCloudProviderMetadata(provider) {
|
|
135
|
+
if (provider && FETCHERS[provider]) return FETCHERS[provider]();
|
|
136
|
+
|
|
137
|
+
const results = await Promise.allSettled(Object.values(FETCHERS).map(fn => fn()));
|
|
138
|
+
for (const result of results) {
|
|
139
|
+
if (result.status === 'fulfilled' && result.value !== null) {
|
|
140
|
+
return result.value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
const { expect } = require('chai');
|
|
6
|
+
const proxyquire = require('proxyquire');
|
|
7
|
+
const { CanceledError } = require('axios');
|
|
8
|
+
|
|
9
|
+
describe('core system-result cloud-provider-metadata', function () {
|
|
10
|
+
/** @type {sinon.SinonStub} */
|
|
11
|
+
let axios;
|
|
12
|
+
/** @type {import('./cloud-provider-metadata').getCloudProviderMetadata} */
|
|
13
|
+
let getCloudProviderMetadata;
|
|
14
|
+
|
|
15
|
+
beforeEach(function () {
|
|
16
|
+
axios = sinon.stub().rejects();
|
|
17
|
+
|
|
18
|
+
getCloudProviderMetadata = proxyquire('./cloud-provider-metadata', {
|
|
19
|
+
axios: { default: axios }
|
|
20
|
+
}).getCloudProviderMetadata;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('AWS', function () {
|
|
24
|
+
it('handles aws', async function () {
|
|
25
|
+
axios.onCall(0).resolves({ data: 'foo' });
|
|
26
|
+
axios.onCall(1).resolves({ data: { region: 'us-east1', accountId: '123456789', instanceId: 'i-012345678' } });
|
|
27
|
+
|
|
28
|
+
const result = await getCloudProviderMetadata('AWS');
|
|
29
|
+
expect(axios).to.have.callCount(2);
|
|
30
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
31
|
+
method: 'PUT',
|
|
32
|
+
url: 'http://169.254.169.254/latest/api/token',
|
|
33
|
+
headers: {
|
|
34
|
+
'X-aws-ec2-metadata-token-ttl-seconds': '300'
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
38
|
+
method: 'GET',
|
|
39
|
+
url: 'http://169.254.169.254/latest/dynamic/instance-identity/document',
|
|
40
|
+
headers: {
|
|
41
|
+
'X-aws-ec2-metadata-token': 'foo'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
expect(result).to.deep.equal({
|
|
45
|
+
provider: 'aws',
|
|
46
|
+
id: 'arn:aws:ec2:us-east1:123456789:instance/i-012345678',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles errors', async function () {
|
|
51
|
+
axios.onCall(0).resolves({ data: 'foo' });
|
|
52
|
+
axios.onCall(1).rejects();
|
|
53
|
+
|
|
54
|
+
const result = await getCloudProviderMetadata('AWS');
|
|
55
|
+
expect(axios).to.have.callCount(2);
|
|
56
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
57
|
+
method: 'PUT',
|
|
58
|
+
url: 'http://169.254.169.254/latest/api/token',
|
|
59
|
+
headers: {
|
|
60
|
+
'X-aws-ec2-metadata-token-ttl-seconds': '300'
|
|
61
|
+
},
|
|
62
|
+
proxy: false,
|
|
63
|
+
});
|
|
64
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: 'http://169.254.169.254/latest/dynamic/instance-identity/document',
|
|
67
|
+
headers: {
|
|
68
|
+
'X-aws-ec2-metadata-token': 'foo'
|
|
69
|
+
},
|
|
70
|
+
proxy: false,
|
|
71
|
+
});
|
|
72
|
+
expect(result).to.be.null;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Azure', function () {
|
|
77
|
+
it('handles azure', async function () {
|
|
78
|
+
// example from azure docs: https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux#access-azure-instance-metadata-service
|
|
79
|
+
const id = '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname';
|
|
80
|
+
axios.resolves({ data: id });
|
|
81
|
+
|
|
82
|
+
const result = await getCloudProviderMetadata('Azure');
|
|
83
|
+
expect(axios).to.have.callCount(1);
|
|
84
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
85
|
+
method: 'GET',
|
|
86
|
+
url: 'http://169.254.169.254/metadata/instance/compute/resourceId?api-version=2021-02-01&format=text',
|
|
87
|
+
headers: {
|
|
88
|
+
Metadata: 'true'
|
|
89
|
+
},
|
|
90
|
+
proxy: false,
|
|
91
|
+
});
|
|
92
|
+
expect(result).to.deep.equal({ provider: 'azure', id });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles errors', async function () {
|
|
96
|
+
axios.rejects();
|
|
97
|
+
|
|
98
|
+
const result = await getCloudProviderMetadata('Azure');
|
|
99
|
+
expect(axios).to.have.callCount(1);
|
|
100
|
+
expect(result).to.be.null;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('GCP', function () {
|
|
105
|
+
it('handles gcp', async function () {
|
|
106
|
+
axios.resolves({ data: '954399829989587067' });
|
|
107
|
+
|
|
108
|
+
const result = await getCloudProviderMetadata('GCP');
|
|
109
|
+
expect(axios).to.have.callCount(1);
|
|
110
|
+
expect(axios).to.have.been.calledWithMatch({
|
|
111
|
+
method: 'GET',
|
|
112
|
+
url: 'http://169.254.169.254/computeMetadata/v1/instance/id?alt=text',
|
|
113
|
+
headers: {
|
|
114
|
+
'Metadata-Flavor': 'Google'
|
|
115
|
+
},
|
|
116
|
+
proxy: false,
|
|
117
|
+
});
|
|
118
|
+
expect(result).to.deep.equal({ provider: 'gcp', id: '954399829989587067' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('retries if a 503 is returned', async function () {
|
|
122
|
+
axios.onCall(0).rejects({ response: { status: 503 } });
|
|
123
|
+
axios.onCall(1).resolves({ data: '954399829989587067' });
|
|
124
|
+
|
|
125
|
+
const result = await getCloudProviderMetadata('GCP');
|
|
126
|
+
expect(axios).to.have.callCount(2);
|
|
127
|
+
expect(result).to.deep.equal({ provider: 'gcp', id: '954399829989587067' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('handles misc errors', async function () {
|
|
131
|
+
axios.rejects();
|
|
132
|
+
|
|
133
|
+
const result = await getCloudProviderMetadata('GCP');
|
|
134
|
+
expect(axios).to.have.callCount(1);
|
|
135
|
+
expect(result).to.be.null;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('auto-detection', function () {
|
|
140
|
+
this.beforeEach(function () {
|
|
141
|
+
axios.rejects(new CanceledError('canceled'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns null if all responses timeout', async function () {
|
|
145
|
+
const result = await getCloudProviderMetadata(undefined);
|
|
146
|
+
expect(axios).to.have.callCount(3); // 1 aws + 1 azure + 1 gcp
|
|
147
|
+
expect(result).to.be.null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles a single valid response', async function () {
|
|
151
|
+
axios.withArgs(sinon.match({
|
|
152
|
+
url: 'http://169.254.169.254/metadata/instance/compute/resourceId?api-version=2021-02-01&format=text',
|
|
153
|
+
})).resolves({ data: 'azure-resource-identifier' });
|
|
154
|
+
|
|
155
|
+
const result = await getCloudProviderMetadata(undefined);
|
|
156
|
+
expect(axios).to.have.callCount(3); // 1 aws + 1 azure + 1 gcp
|
|
157
|
+
expect(result).to.deep.equal({ provider: 'azure', id: 'azure-resource-identifier' });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
package/lib/system-info/index.js
CHANGED
|
@@ -12,52 +12,47 @@
|
|
|
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
|
+
// @ts-check
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const fs = require('fs');
|
|
18
|
+
const fs = require('fs/promises');
|
|
20
19
|
const os = require('os');
|
|
20
|
+
const { getCloudProviderMetadata } = require('./cloud-provider-metadata');
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
version = require(path.join(packagePath, 'package.json')).dependencies['pm2'];
|
|
31
|
-
} catch (err) {
|
|
32
|
-
//
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (version) break;
|
|
22
|
+
const MOUNTINFO_REGEX = /\/docker\/containers\/(.*?)\//;
|
|
23
|
+
const CGROUP_REGEX = /:\/docker\/([^/]+)$/;
|
|
24
|
+
|
|
25
|
+
function isUsingPM2(pkg) {
|
|
26
|
+
const result = { used: !!process.env.pmx, version: null };
|
|
27
|
+
|
|
28
|
+
if (pkg?.dependences?.['pm2']) {
|
|
29
|
+
result.version = pkg.dependencies['pm2'];
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
return
|
|
32
|
+
return result;
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
function isDocker() {
|
|
42
|
-
const MOUNTINFO_REGEX = /\/docker\/containers\/(.*?)\//;
|
|
43
|
-
const CGROUP_REGEX = /:\/docker\/([^/]+)$/;
|
|
44
|
-
|
|
35
|
+
async function isDocker() {
|
|
45
36
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
37
|
+
const result = await fs.readFile('/proc/self/mountinfo', 'utf8');
|
|
38
|
+
// @ts-expect-error readFile with encoding returns a string, not a Buffer
|
|
39
|
+
const matches = result.match(MOUNTINFO_REGEX);
|
|
40
|
+
if (matches) return { isDocker: true, containerID: matches[1] };
|
|
48
41
|
} catch (err) {
|
|
49
42
|
// else check /proc/self/cgroup
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
46
|
+
const result = await fs.readFile('/proc/self/cgroup', 'utf8');
|
|
47
|
+
// @ts-expect-error readFile with encoding returns a string, not a Buffer
|
|
48
|
+
const matches = result.match(CGROUP_REGEX);
|
|
49
|
+
if (matches) return { isDocker: true, containerID: matches[1] };
|
|
55
50
|
} catch (err) {
|
|
56
51
|
// else check /.dockerenv
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
try {
|
|
60
|
-
const result = fs.
|
|
55
|
+
const result = await fs.stat('/.dockerenv');
|
|
61
56
|
if (result) return { isDocker: true, containerID: null };
|
|
62
57
|
} catch (err) {
|
|
63
58
|
// if there's not such file we can conclude it's not docker env
|
|
@@ -70,13 +65,20 @@ module.exports = function(core) {
|
|
|
70
65
|
const {
|
|
71
66
|
agentName,
|
|
72
67
|
agentVersion,
|
|
73
|
-
config
|
|
68
|
+
config,
|
|
69
|
+
appInfo,
|
|
74
70
|
} = core;
|
|
75
71
|
|
|
76
72
|
// have values default to null so all required keys get serialized
|
|
77
|
-
core.getSystemInfo = function() {
|
|
78
|
-
|
|
73
|
+
core.getSystemInfo = async function getSystemInfo() {
|
|
74
|
+
// memoize for subsequent lookups
|
|
75
|
+
if (core._systemInfo) return core._systemInfo;
|
|
79
76
|
|
|
77
|
+
const cpus = os.cpus();
|
|
78
|
+
const totalmem = os.totalmem();
|
|
79
|
+
const freemem = os.freemem();
|
|
80
|
+
|
|
81
|
+
/** @type {import('@contrast/common').SystemInfo} */
|
|
80
82
|
const info = {
|
|
81
83
|
ReportDate: new Date().toISOString(),
|
|
82
84
|
MachineName: os.hostname(),
|
|
@@ -95,7 +97,8 @@ module.exports = function(core) {
|
|
|
95
97
|
},
|
|
96
98
|
},
|
|
97
99
|
Node: {
|
|
98
|
-
|
|
100
|
+
Path: process.execPath,
|
|
101
|
+
Version: process.version,
|
|
99
102
|
},
|
|
100
103
|
OperatingSystem: {
|
|
101
104
|
Architecture: os.arch(),
|
|
@@ -103,22 +106,34 @@ module.exports = function(core) {
|
|
|
103
106
|
Version: os.release(),
|
|
104
107
|
KernelVersion: os.version(),
|
|
105
108
|
CPU: {
|
|
106
|
-
Type:
|
|
107
|
-
Count:
|
|
109
|
+
Type: cpus[0].model,
|
|
110
|
+
Count: cpus.length,
|
|
108
111
|
}
|
|
109
112
|
},
|
|
110
113
|
Host: {
|
|
111
|
-
Docker: isDocker(),
|
|
112
|
-
PM2: isUsingPM2(),
|
|
114
|
+
Docker: await isDocker(),
|
|
115
|
+
PM2: isUsingPM2(appInfo.pkg),
|
|
113
116
|
Memory: {
|
|
114
|
-
Total: (
|
|
115
|
-
Free: (
|
|
116
|
-
Used: ((
|
|
117
|
+
Total: (totalmem / 1e6).toFixed(0).concat(' MB'),
|
|
118
|
+
Free: (freemem / 1e6).toFixed(0).concat(' MB'),
|
|
119
|
+
Used: ((totalmem - freemem) / 1e6).toFixed(0).concat(' MB'),
|
|
117
120
|
}
|
|
118
121
|
},
|
|
119
|
-
Application:
|
|
122
|
+
Application: appInfo.pkg,
|
|
123
|
+
Cloud: {
|
|
124
|
+
Provider: null,
|
|
125
|
+
ResourceID: null,
|
|
126
|
+
}
|
|
120
127
|
};
|
|
121
128
|
|
|
122
|
-
|
|
129
|
+
if (config.server.discover_cloud_resource) {
|
|
130
|
+
const metadata = await getCloudProviderMetadata(config.inventory.gather_metadata_via);
|
|
131
|
+
if (metadata) {
|
|
132
|
+
info.Cloud.Provider = metadata.provider;
|
|
133
|
+
info.Cloud.ResourceID = metadata.id;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return core._systemInfo = info;
|
|
123
138
|
};
|
|
124
139
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
const proxyquire = require('proxyquire');
|
|
6
|
+
|
|
7
|
+
describe('core system-info', function () {
|
|
8
|
+
let core;
|
|
9
|
+
|
|
10
|
+
beforeEach(function () {
|
|
11
|
+
core = require('@contrast/test/mocks/core')();
|
|
12
|
+
core.config = require('@contrast/test/mocks/config')();
|
|
13
|
+
|
|
14
|
+
proxyquire('.', {
|
|
15
|
+
'./cloud-provider-metadata': {
|
|
16
|
+
getCloudProviderMetadata: sinon.stub().resolves(null)
|
|
17
|
+
}
|
|
18
|
+
})(core);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns a properly structured object', async function () {
|
|
22
|
+
const info = await core.getSystemInfo();
|
|
23
|
+
|
|
24
|
+
expect(info).to.not.be.undefined;
|
|
25
|
+
expect(info).to.haveOwnProperty('ReportDate');
|
|
26
|
+
expect(info).to.haveOwnProperty('MachineName');
|
|
27
|
+
expect(info).to.haveOwnProperty('Contrast');
|
|
28
|
+
expect(info).to.haveOwnProperty('Node');
|
|
29
|
+
expect(info).to.haveOwnProperty('OperatingSystem');
|
|
30
|
+
expect(info).to.haveOwnProperty('Host');
|
|
31
|
+
expect(info).to.haveOwnProperty('Application');
|
|
32
|
+
expect(info).to.haveOwnProperty('Cloud');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.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)",
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
"test": "../scripts/test.sh"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@contrast/common": "1.
|
|
19
|
+
"@contrast/common": "1.23.0",
|
|
20
20
|
"@contrast/find-package-json": "^1.0.0",
|
|
21
|
-
"@contrast/fn-inspect": "^4.0.0"
|
|
21
|
+
"@contrast/fn-inspect": "^4.0.0",
|
|
22
|
+
"axios": "^1.6.8"
|
|
22
23
|
}
|
|
23
24
|
}
|