@contrast/core 1.33.0 → 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.
@@ -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
+ });
@@ -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
+ });
@@ -25,11 +25,11 @@ const abort = (ms) => {
25
25
  return abortController.signal;
26
26
  };
27
27
 
28
+ /** @param {number} ms */
29
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
30
+
28
31
  const FETCHERS = {
29
- /**
30
- * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
31
- * @returns {Promise<string | null>}
32
- */
32
+ /** @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html */
33
33
  async AWS() {
34
34
  try {
35
35
  const { data: token } = await axios({
@@ -38,19 +38,25 @@ const FETCHERS = {
38
38
  headers: {
39
39
  'X-aws-ec2-metadata-token-ttl-seconds': '300'
40
40
  },
41
+ proxy: false, // proxies should not be used in any cloud provider.
41
42
  signal: abort(5000),
42
43
  });
43
- const { data } = await axios({
44
+ const { data: document } = await axios({
44
45
  method: 'GET',
45
46
  url: new URL('/latest/dynamic/instance-identity/document', METADATA_ENDPOINT_ADDRESS).href,
46
47
  headers: {
47
48
  'X-aws-ec2-metadata-token': token
48
49
  },
50
+ proxy: false,
49
51
  signal: abort(5000),
50
52
  });
51
- if (data) {
52
- const { region, accountId, instanceId } = data;
53
- return `arn:aws:ec2:${region}:${accountId}:instance/${instanceId}`;
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
+ };
54
60
  }
55
61
  } catch {
56
62
  // ignore, return null
@@ -58,36 +64,74 @@ const FETCHERS = {
58
64
 
59
65
  return null;
60
66
  },
61
- /**
62
- * @see https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux
63
- * @returns {Promise<string | null>}
64
- */
67
+ /** @see https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service?tabs=linux */
65
68
  async Azure() {
66
69
  try {
67
- const { data } = await axios({
70
+ const { data: resourceId } = await axios({
68
71
  method: 'GET',
69
72
  url: new URL('/metadata/instance/compute/resourceId?api-version=2021-02-01&format=text', METADATA_ENDPOINT_ADDRESS).href,
70
73
  headers: {
71
74
  Metadata: 'true'
72
75
  },
73
- proxy: false, // You *must* bypass proxies when querying IMDS
76
+ proxy: false,
74
77
  signal: abort(5000),
75
78
  });
76
- if (data) return data;
79
+
80
+ if (resourceId) {
81
+ return {
82
+ provider: 'azure',
83
+ id: resourceId,
84
+ };
85
+ }
77
86
  } catch {
78
87
  // ignore, return null
79
88
  }
80
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
+
81
120
  return null;
82
121
  }
83
122
  };
84
123
 
85
124
  module.exports = {
86
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
+ *
87
131
  * @param {import('@contrast/config').Config['inventory']['gather_metadata_via']} provider
88
- * @returns {Promise<string | null>}
132
+ * @returns {Promise<{ provider: string, id: string } | null>}
89
133
  */
90
- async getResourceID(provider) {
134
+ async getCloudProviderMetadata(provider) {
91
135
  if (provider && FETCHERS[provider]) return FETCHERS[provider]();
92
136
 
93
137
  const results = await Promise.allSettled(Object.values(FETCHERS).map(fn => fn()));
@@ -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
+ });
@@ -17,7 +17,7 @@
17
17
 
18
18
  const fs = require('fs/promises');
19
19
  const os = require('os');
20
- const { getResourceID } = require('./cloud-resource-identifier');
20
+ const { getCloudProviderMetadata } = require('./cloud-provider-metadata');
21
21
 
22
22
  const MOUNTINFO_REGEX = /\/docker\/containers\/(.*?)\//;
23
23
  const CGROUP_REGEX = /:\/docker\/([^/]+)$/;
@@ -35,6 +35,7 @@ function isUsingPM2(pkg) {
35
35
  async function isDocker() {
36
36
  try {
37
37
  const result = await fs.readFile('/proc/self/mountinfo', 'utf8');
38
+ // @ts-expect-error readFile with encoding returns a string, not a Buffer
38
39
  const matches = result.match(MOUNTINFO_REGEX);
39
40
  if (matches) return { isDocker: true, containerID: matches[1] };
40
41
  } catch (err) {
@@ -43,6 +44,7 @@ async function isDocker() {
43
44
 
44
45
  try {
45
46
  const result = await fs.readFile('/proc/self/cgroup', 'utf8');
47
+ // @ts-expect-error readFile with encoding returns a string, not a Buffer
46
48
  const matches = result.match(CGROUP_REGEX);
47
49
  if (matches) return { isDocker: true, containerID: matches[1] };
48
50
  } catch (err) {
@@ -76,6 +78,7 @@ module.exports = function(core) {
76
78
  const totalmem = os.totalmem();
77
79
  const freemem = os.freemem();
78
80
 
81
+ /** @type {import('@contrast/common').SystemInfo} */
79
82
  const info = {
80
83
  ReportDate: new Date().toISOString(),
81
84
  MachineName: os.hostname(),
@@ -94,7 +97,8 @@ module.exports = function(core) {
94
97
  },
95
98
  },
96
99
  Node: {
97
- Version: process.version
100
+ Path: process.execPath,
101
+ Version: process.version,
98
102
  },
99
103
  OperatingSystem: {
100
104
  Architecture: os.arch(),
@@ -116,10 +120,18 @@ module.exports = function(core) {
116
120
  }
117
121
  },
118
122
  Application: appInfo.pkg,
123
+ Cloud: {
124
+ Provider: null,
125
+ ResourceID: null,
126
+ }
119
127
  };
120
128
 
121
129
  if (config.server.discover_cloud_resource) {
122
- info.ResourceID = await getResourceID(config.inventory.gather_metadata_via);
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
+ }
123
135
  }
124
136
 
125
137
  return core._systemInfo = info;
@@ -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.33.0",
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,7 +16,7 @@
16
16
  "test": "../scripts/test.sh"
17
17
  },
18
18
  "dependencies": {
19
- "@contrast/common": "1.22.0",
19
+ "@contrast/common": "1.23.0",
20
20
  "@contrast/find-package-json": "^1.0.0",
21
21
  "@contrast/fn-inspect": "^4.0.0",
22
22
  "axios": "^1.6.8"