@contrast/agent 4.10.6 → 4.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/VERSION +1 -1
- package/bin/linux/contrast-service +0 -0
- package/bin/mac/contrast-service +0 -0
- package/bin/windows/contrast-service.exe +0 -0
- package/lib/agent.js +9 -2
- package/lib/app-info.js +88 -52
- package/lib/assess/loopback4/route-coverage.js +1 -1
- package/lib/contrast.js +25 -24
- package/lib/core/config/options.js +136 -238
- package/lib/hooks/frameworks/base.js +1 -1
- package/lib/hooks/module/helpers.js +1 -1
- package/lib/instrumentation.js +1 -1
- package/lib/libraries.js +119 -135
- package/lib/list-installed.js +13 -0
- package/lib/reporter/models/app-update/index.js +1 -1
- package/lib/reporter/translations/to-protobuf/dtm/agent-startup.js +3 -3
- package/lib/telemetry.js +188 -0
- package/lib/util/traverse.js +7 -3
- package/package.json +11 -9
|
@@ -66,7 +66,7 @@ class BaseFramework {
|
|
|
66
66
|
|
|
67
67
|
if (frame) {
|
|
68
68
|
const { file, lineNumber } = frame;
|
|
69
|
-
const relativeFile = (file || '').replace(this.agent.appInfo.
|
|
69
|
+
const relativeFile = (file || '').replace(this.agent.appInfo.appDir, '');
|
|
70
70
|
return `${relativeFile}:${lineNumber || ''}`;
|
|
71
71
|
} else {
|
|
72
72
|
return null;
|
|
@@ -36,7 +36,7 @@ let appDirRegex;
|
|
|
36
36
|
* @returns {string}
|
|
37
37
|
*/
|
|
38
38
|
const getCachedFilename = (agent, filename) => {
|
|
39
|
-
appDirRegex = appDirRegex || new RegExp(`^(${agent.appInfo.
|
|
39
|
+
appDirRegex = appDirRegex || new RegExp(`^(${agent.appInfo.appDir})?`);
|
|
40
40
|
if (os.platform() === 'win32') {
|
|
41
41
|
filename = filename.replace(/C:/i, '');
|
|
42
42
|
}
|
package/lib/instrumentation.js
CHANGED
|
@@ -89,7 +89,7 @@ function collectLibInfo(agent) {
|
|
|
89
89
|
require('./hooks/module/extensions')();
|
|
90
90
|
libUsage.listen(evalFrequency);
|
|
91
91
|
}
|
|
92
|
-
libraries.
|
|
92
|
+
libraries.getLibInfo(agent, eluEnabled).then((libs) => {
|
|
93
93
|
agentEmitter.emit('appinfo', { libs });
|
|
94
94
|
});
|
|
95
95
|
}
|
package/lib/libraries.js
CHANGED
|
@@ -13,176 +13,160 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
|
-
const Promise = require('bluebird');
|
|
17
|
-
const stringify = require('json-stable-stringify');
|
|
18
|
-
const _ = require('lodash');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const util = require('util');
|
|
21
16
|
|
|
22
|
-
const {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
17
|
+
const { stat } = require('fs').promises;
|
|
18
|
+
const stringify = require('json-stable-stringify');
|
|
19
|
+
const Module = require('module');
|
|
20
|
+
const { AGENT_INFO } = require('./constants');
|
|
26
21
|
const { runInNoInstrumentationScope } = require('./core/async-storage/scopes');
|
|
22
|
+
const logger = require('./core/logger')('contrast:libraries');
|
|
27
23
|
const listInstalled = require('./list-installed');
|
|
28
24
|
const AppUpdate = require('./reporter/models/app-update');
|
|
29
25
|
|
|
30
26
|
const DEADZONE_NAME = 'agent: libraries';
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const stat = util.promisify(require('fs').stat);
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {import('./list-installed').Result} Result
|
|
30
|
+
*/
|
|
36
31
|
|
|
37
32
|
/**
|
|
38
|
-
*
|
|
39
|
-
* @param {
|
|
33
|
+
* Searches a list of paths and returns the first valid path found.
|
|
34
|
+
* @param {string[]} paths
|
|
35
|
+
* @returns {Promise<string | undefined>}
|
|
36
|
+
*/
|
|
37
|
+
const findNodeModsPath = async (paths) =>
|
|
38
|
+
// this functions similarly to `Bluebird.map() with concurrency: 1
|
|
39
|
+
paths.reduce(async (prev, path) => {
|
|
40
|
+
if (await prev) return prev;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await stat(path);
|
|
44
|
+
return path;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}, Promise.resolve());
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Filters out the agent as a dependency, formats libraries, and keeps track of
|
|
52
|
+
* nested modules.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ [key: string]: Result | string }} deps collection of dependencies from app root
|
|
40
55
|
* @param {Map<string, Set<string>} requiredBy
|
|
41
|
-
* @
|
|
56
|
+
* @param {string} parent the name of the module requiring the current when nested
|
|
57
|
+
* @return {{ [key: string]: Result }} formatted object
|
|
42
58
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
set.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const formatDependencies = (deps, requiredBy, parent) =>
|
|
60
|
+
Object.entries(deps || {}).reduce((result, [key, val]) => {
|
|
61
|
+
if (key === AGENT_INFO.NAME) {
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (parent) {
|
|
66
|
+
const set = requiredBy.has(key) ? requiredBy.get(key) : new Set();
|
|
67
|
+
|
|
68
|
+
set.add(parent);
|
|
69
|
+
requiredBy.set(key, set);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof val === 'string') {
|
|
73
|
+
result[key] = {
|
|
74
|
+
name: key,
|
|
75
|
+
version: val,
|
|
76
|
+
dependencies: {}
|
|
77
|
+
};
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!val.name) val.name = key;
|
|
82
|
+
|
|
83
|
+
val.dependencies = formatDependencies(val.dependencies, requiredBy, key);
|
|
84
|
+
result[key] = val;
|
|
56
85
|
|
|
57
|
-
|
|
58
|
-
}
|
|
86
|
+
return result;
|
|
87
|
+
}, {});
|
|
59
88
|
|
|
60
89
|
/**
|
|
61
90
|
* Recursively adds each dependency as a library which is used later
|
|
62
91
|
* for reporting inventory and class usage
|
|
63
92
|
*
|
|
64
|
-
* @param {
|
|
93
|
+
* @param {{ [key: string]: Result }} deps the formatted object from formatDependencies
|
|
65
94
|
* @param {Map<string, Set<string>} requiredBy map containing data regarding dependents
|
|
66
|
-
* @param {
|
|
95
|
+
* @param {import('./agent')} agent agent instance
|
|
67
96
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const dep = deps[key];
|
|
71
|
-
if (!dep.name) {
|
|
72
|
-
dep.name = key;
|
|
73
|
-
}
|
|
74
|
-
|
|
97
|
+
const processDependencies = (deps, requiredBy, agent) => {
|
|
98
|
+
Object.values(deps).forEach((dep) => {
|
|
75
99
|
dep._requiredBy = requiredBy.has(dep.name)
|
|
76
100
|
? Array.from(requiredBy.get(dep.name))
|
|
77
101
|
: [];
|
|
78
102
|
|
|
79
103
|
const newLib = AppUpdate.addLib(dep, agent);
|
|
80
|
-
// check its deps as well
|
|
104
|
+
// If this is the first time we've checked this dep, check its deps as well.
|
|
81
105
|
if (newLib) {
|
|
82
106
|
processDependencies(dep.dependencies, requiredBy, agent);
|
|
83
107
|
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* compares module name with agent name
|
|
89
|
-
*
|
|
90
|
-
* @param {string} modeName
|
|
91
|
-
* @return {Boolean} if modName matches agent, returns true
|
|
92
|
-
*/
|
|
93
|
-
function isAgent(modName) {
|
|
94
|
-
return modName === NAME;
|
|
95
|
-
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
96
110
|
|
|
97
111
|
/**
|
|
98
|
-
*
|
|
99
|
-
* Formats libraries as { 'mod-name': { <mod package.json deets } }
|
|
112
|
+
* Traverses entire dependency tree to report library inventory
|
|
100
113
|
*
|
|
101
|
-
* @param {
|
|
102
|
-
* @
|
|
114
|
+
* @param {import('./agent')} agent agent instance
|
|
115
|
+
* @param {boolean} eluEnabled flag to get composition for each module
|
|
116
|
+
* @returns {AppUpdate.libraries}
|
|
103
117
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
const getLibInfo = async (agent, eluEnabled) =>
|
|
119
|
+
runInNoInstrumentationScope(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const boundRequire = Module.createRequire(agent.appInfo.path);
|
|
122
|
+
const paths = boundRequire.resolve.paths(agent.appInfo.path);
|
|
123
|
+
const nodeModsPath = await findNodeModsPath(paths);
|
|
124
|
+
|
|
125
|
+
if (!nodeModsPath) {
|
|
126
|
+
logger.error(
|
|
127
|
+
`unable to read installed dependencies because a node_modules directory could not be detected given a package.json located at %s - use the agent.node.app_root configuration variable if installed in non-standard location`,
|
|
128
|
+
agent.appInfo.path
|
|
129
|
+
);
|
|
130
|
+
return AppUpdate.libraries;
|
|
110
131
|
}
|
|
111
132
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
133
|
+
logger.debug('detected node_modules at %s', nodeModsPath);
|
|
134
|
+
const data = await listInstalled(nodeModsPath, logger);
|
|
135
|
+
|
|
136
|
+
logger.debug(
|
|
137
|
+
'package.json dependencies: %s',
|
|
138
|
+
// _dependencies contains { name: version } info
|
|
139
|
+
stringify(data._dependencies, { space: 2 })
|
|
140
|
+
);
|
|
141
|
+
logger.debug(
|
|
142
|
+
'package.json devDependencies: %s',
|
|
143
|
+
stringify(data.devDependencies, { space: 2 })
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const requiredBy = new Map();
|
|
147
|
+
const dependencies = formatDependencies(data.dependencies, requiredBy);
|
|
148
|
+
processDependencies(dependencies, requiredBy, agent);
|
|
149
|
+
|
|
150
|
+
const libs = AppUpdate.libraries;
|
|
151
|
+
if (eluEnabled) {
|
|
152
|
+
logger.info('recursively scanning dependencies');
|
|
153
|
+
// this functions similarly to `Bluebird.map()` with concurrency: 1
|
|
154
|
+
await Object.values(libs).reduce(async (prev, lib) => {
|
|
155
|
+
await prev;
|
|
156
|
+
return lib.getComposition();
|
|
157
|
+
}, Promise.resolve());
|
|
119
158
|
}
|
|
120
159
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
160
|
+
logger.info(
|
|
161
|
+
'finished library inventory for %s dependencies',
|
|
162
|
+
Object.keys(libs).length
|
|
163
|
+
);
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*/
|
|
133
|
-
function getPackageLibraries(agent, eluEnabled) {
|
|
134
|
-
const appRoot = _.get(agent, 'appInfo.path');
|
|
135
|
-
return runInNoInstrumentationScope(() => {
|
|
136
|
-
const dir = path.dirname(appRoot);
|
|
137
|
-
|
|
138
|
-
let nodeModsExists = false;
|
|
139
|
-
return stat(`${dir + path.sep}node_modules`)
|
|
140
|
-
.then(() => {
|
|
141
|
-
nodeModsExists = true;
|
|
142
|
-
return listInstalled(dir, logger);
|
|
143
|
-
})
|
|
144
|
-
.then((data) => {
|
|
145
|
-
logger.debug(
|
|
146
|
-
'package.json dependencies: %s',
|
|
147
|
-
stringify(data._dependencies, { space: 2 })
|
|
148
|
-
);
|
|
149
|
-
logger.debug(
|
|
150
|
-
'package.json devDependencies: %s',
|
|
151
|
-
stringify(data.devDependencies, { space: 2 })
|
|
152
|
-
);
|
|
153
|
-
const dependencies = formatDependencies(data.dependencies);
|
|
154
|
-
const requiredBy = setRequiredBy(dependencies);
|
|
155
|
-
return processDependencies(dependencies, requiredBy, agent);
|
|
156
|
-
})
|
|
157
|
-
.then(() => {
|
|
158
|
-
if (eluEnabled) {
|
|
159
|
-
logger.info('recursively scanning dependencies');
|
|
160
|
-
const libs = _.values(AppUpdate.libraries);
|
|
161
|
-
return Promise.map(libs, (lib) => lib.getComposition(), {
|
|
162
|
-
concurrency: 1
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
.then(() => {
|
|
167
|
-
const libs = AppUpdate.libraries;
|
|
168
|
-
logger.info(
|
|
169
|
-
'finished library inventory for %s dependencies',
|
|
170
|
-
Object.keys(libs).length
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
return libs;
|
|
174
|
-
})
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
if (!nodeModsExists) {
|
|
177
|
-
logger.error(
|
|
178
|
-
`unable to read install dependencies because node_modules are not installed in app root (${dir}) - use agent.node.app_root configuration if installed in non-standard location`
|
|
179
|
-
);
|
|
180
|
-
} else {
|
|
181
|
-
logger.error('unable to read installed dependencies. %o', err);
|
|
182
|
-
}
|
|
183
|
-
return AppUpdate.libraries;
|
|
184
|
-
});
|
|
165
|
+
return libs;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.error('unable to read installed dependencies. %o', err);
|
|
168
|
+
return AppUpdate.libraries;
|
|
169
|
+
}
|
|
185
170
|
}, DEADZONE_NAME);
|
|
186
|
-
}
|
|
187
171
|
|
|
188
|
-
module.exports.
|
|
172
|
+
module.exports.getLibInfo = getLibInfo;
|
package/lib/list-installed.js
CHANGED
|
@@ -21,6 +21,19 @@ const {
|
|
|
21
21
|
|
|
22
22
|
const execFile = util.promisify(require('child_process').execFile);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Result
|
|
26
|
+
* @property {string} name
|
|
27
|
+
* @property {string} version
|
|
28
|
+
* @property {{ [key: string]: Result | string }} dependencies
|
|
29
|
+
* @property {{ [key: string]: Result | string }} devDependencies
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} cwd directory in which we want to execute `npm ls`
|
|
34
|
+
* @param {*} logger
|
|
35
|
+
* @returns {Promise<Result>}
|
|
36
|
+
*/
|
|
24
37
|
module.exports = async function listInstalled(cwd, logger) {
|
|
25
38
|
const env = { ...process.env, NODE_OPTIONS: undefined };
|
|
26
39
|
const args = ['--silent', 'ls', '--json', '--prod', '--long'];
|
|
@@ -22,18 +22,18 @@ const {
|
|
|
22
22
|
|
|
23
23
|
module.exports = function AgentStartup({
|
|
24
24
|
config = { server: {} },
|
|
25
|
-
appInfo: {
|
|
25
|
+
appInfo: { appDir, serverEnvironment, serverVersion, version } = {}
|
|
26
26
|
} = {}) {
|
|
27
27
|
const serverName = config.server.name;
|
|
28
28
|
const { tags, type } = config.server;
|
|
29
|
-
|
|
29
|
+
appDir = config.server.path || appDir;
|
|
30
30
|
|
|
31
31
|
return new dtm.AgentStartup({
|
|
32
32
|
1: VERSION, // 2 version
|
|
33
33
|
2: serverEnvironment, // 3 environment
|
|
34
34
|
3: tags, // 4 tags
|
|
35
35
|
4: serverName, // 5 server_name
|
|
36
|
-
5:
|
|
36
|
+
5: appDir, // 6 server_path
|
|
37
37
|
6: type, // 7 server_type
|
|
38
38
|
7: serverVersion // 8 server_version
|
|
39
39
|
});
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2022 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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { default: axios } = require('axios');
|
|
18
|
+
const { createHash } = require('crypto');
|
|
19
|
+
const findCacheDir = require('find-cache-dir');
|
|
20
|
+
const { promises: fs } = require('fs');
|
|
21
|
+
const { default: getMac } = require('getmac');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const process = require('process');
|
|
25
|
+
const { v4: uuid } = require('uuid');
|
|
26
|
+
const { AGENT_INFO } = require('./constants');
|
|
27
|
+
const logger = require('./core/logger')('contrast:telemetry');
|
|
28
|
+
|
|
29
|
+
const TELEMETRY_URL = 'https://telemetry.nodejs.contrastsecurity.com/';
|
|
30
|
+
const TELEMETRY_VERSION = 'v0'; // TODO: set this to v1 when format is established.
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
CONTRAST_AGENT_TELEMETRY_OPTOUT,
|
|
34
|
+
CONTRAST_DEV,
|
|
35
|
+
CONTRAST_SCREENER
|
|
36
|
+
} = process.env;
|
|
37
|
+
|
|
38
|
+
// TODO: LINK
|
|
39
|
+
const DISCLAIMER = `The Contrast Node Agent collects usage data in order to help us improve compatibility and security coverage.
|
|
40
|
+
The data is anonymous and does not contain application data. It is collected by Contrast and is never shared.
|
|
41
|
+
You can opt-out of telemetry by setting the CONTRAST_AGENT_TELEMETRY_OPTOUT environment variable to '1' or 'true'.
|
|
42
|
+
`;
|
|
43
|
+
// Read more about the Node Agent's telemetry: TODO
|
|
44
|
+
// `;
|
|
45
|
+
|
|
46
|
+
const LOG_MESSAGE = `Telemetry is now active.
|
|
47
|
+
You can opt-out of telemetry by setting the CONTRAST_AGENT_TELEMETRY_OPTOUT environment variable to '1' or 'true'.
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
module.exports = class Telemetry {
|
|
51
|
+
/**
|
|
52
|
+
* @param {import('./agent')} agent
|
|
53
|
+
* @param {any} config
|
|
54
|
+
*/
|
|
55
|
+
constructor(agent, config) {
|
|
56
|
+
const { appInfo } = agent;
|
|
57
|
+
this.appInfo = appInfo;
|
|
58
|
+
this.config = config;
|
|
59
|
+
this.disabled =
|
|
60
|
+
CONTRAST_AGENT_TELEMETRY_OPTOUT === 'true' ||
|
|
61
|
+
CONTRAST_AGENT_TELEMETRY_OPTOUT === '1';
|
|
62
|
+
|
|
63
|
+
if (this.disabled) {
|
|
64
|
+
logger.info(
|
|
65
|
+
'Telemetry opt-out in effect. All usage data collection is suppressed.'
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.printDisclaimer();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Returns the "first" valid MAC address for the machine, throwing if none
|
|
73
|
+
// is found.
|
|
74
|
+
const mac = getMac();
|
|
75
|
+
|
|
76
|
+
const hash = createHash('sha256').update(mac);
|
|
77
|
+
this.instanceId = hash.copy().digest('hex');
|
|
78
|
+
this.applicationId = hash.update(appInfo.name).digest('hex');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// If getMac fails we fall back to generating a UUID. "Unstable"
|
|
81
|
+
// identifiers such as these are prefixed with an underscore.
|
|
82
|
+
const id = uuid();
|
|
83
|
+
|
|
84
|
+
this.instanceId = `_${id}`;
|
|
85
|
+
this.applicationId = this.instanceId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.metrics = this.createMetricsClient();
|
|
89
|
+
this.tags = {
|
|
90
|
+
isCustomerEnv: this.isCustomerEnv(),
|
|
91
|
+
applicationId: this.applicationId,
|
|
92
|
+
osArch: appInfo.os.architecture,
|
|
93
|
+
osPlatform: appInfo.os.platform,
|
|
94
|
+
osRelease: appInfo.os.release,
|
|
95
|
+
isContainer: appInfo.isContainer,
|
|
96
|
+
agent: AGENT_INFO.NAME,
|
|
97
|
+
agentVersion: AGENT_INFO.VERSION,
|
|
98
|
+
isAssess: agent.isInAssessMode(),
|
|
99
|
+
isProtect: agent.isInDefendMode(),
|
|
100
|
+
nodeVersion: appInfo.nodeVersion,
|
|
101
|
+
nodeVersionMajor: appInfo.nodeVersionMajor,
|
|
102
|
+
telemetryVersion: TELEMETRY_VERSION
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.sendStartupData();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async printDisclaimer() {
|
|
109
|
+
const filePath = path.resolve(
|
|
110
|
+
findCacheDir({ name: AGENT_INFO.NAME }) || os.tmpdir(),
|
|
111
|
+
'.telemetry-disclaimer'
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fs.stat(filePath);
|
|
116
|
+
// If we've previously shown our disclaimer we log the shorter message.
|
|
117
|
+
logger.info(LOG_MESSAGE);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// When there's no file `stat` throws an error. That means we haven't
|
|
120
|
+
// yet shown our disclaimer.
|
|
121
|
+
console.log(DISCLAIMER);
|
|
122
|
+
logger.info(DISCLAIMER);
|
|
123
|
+
try {
|
|
124
|
+
await fs.writeFile(filePath, DISCLAIMER);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// if we can't write, oh well.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {import('axios').AxiosInstance}
|
|
133
|
+
*/
|
|
134
|
+
createMetricsClient() {
|
|
135
|
+
const userAgent = `${AGENT_INFO.NAME}/${AGENT_INFO.VERSION}`;
|
|
136
|
+
|
|
137
|
+
return axios.create({
|
|
138
|
+
baseURL: new URL('/api/v1/telemetry/metrics/node', TELEMETRY_URL).href,
|
|
139
|
+
headers: {
|
|
140
|
+
'User-Agent': userAgent
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Logic to determine whether we are reporting a customer's application.
|
|
147
|
+
* Naive for the time being.
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
isCustomerEnv() {
|
|
151
|
+
if (CONTRAST_SCREENER || CONTRAST_DEV) return false;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} path
|
|
157
|
+
* @param {{ [field: string]: number ]}} fields
|
|
158
|
+
*/
|
|
159
|
+
async sendMetrics(path, fields) {
|
|
160
|
+
return this.metrics.post(path, [
|
|
161
|
+
{
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
instance: this.instanceId,
|
|
164
|
+
tags: this.tags,
|
|
165
|
+
fields
|
|
166
|
+
}
|
|
167
|
+
]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async sendStartupData() {
|
|
171
|
+
try {
|
|
172
|
+
// snapshotted when we send our startup data.
|
|
173
|
+
const memoryUsage = process.memoryUsage();
|
|
174
|
+
|
|
175
|
+
await this.sendMetrics('startup', {
|
|
176
|
+
numCpus: os.cpus().length,
|
|
177
|
+
memTotal: os.totalmem(),
|
|
178
|
+
memHeapTotal: memoryUsage.heapTotal,
|
|
179
|
+
memHeapUsed: memoryUsage.heapUsed,
|
|
180
|
+
memExternal: memoryUsage.external,
|
|
181
|
+
memRss: memoryUsage.rss,
|
|
182
|
+
uptime: process.uptime()
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
logger.warn('telemetry update failed: %s', err.toJSON());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
package/lib/util/traverse.js
CHANGED
|
@@ -158,24 +158,28 @@ function traverse(obj, fn, visitor, maxDepth) {
|
|
|
158
158
|
traverseObject(obj, visitor, maxDepth);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function traverseObject(obj, visitor, remainingDepth =
|
|
162
|
-
if (!obj) {
|
|
161
|
+
function traverseObject(obj, visitor, remainingDepth = 250, visited = new Set()) {
|
|
162
|
+
if (!obj || visited.has(obj)) {
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
|
+
|
|
165
166
|
if (visitor.path.length() < 1) {
|
|
166
167
|
visitor.visit('', obj);
|
|
168
|
+
visited.add(obj);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
// This is done just to reset the stack, without changing the current impletion
|
|
170
172
|
if (remainingDepth == 0) {
|
|
171
173
|
visitor.visit(null, null, true);
|
|
174
|
+
visited.add(obj);
|
|
172
175
|
return;
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
Object.entries(obj).forEach(([key, value]) => {
|
|
176
179
|
visitor.visit(key, value);
|
|
180
|
+
visited.add(obj);
|
|
177
181
|
if (value && typeof value === 'object') {
|
|
178
|
-
traverseObject(value, visitor, remainingDepth - 1);
|
|
182
|
+
traverseObject(value, visitor, remainingDepth - 1, visited);
|
|
179
183
|
}
|
|
180
184
|
});
|
|
181
185
|
}
|