@contrast/core 1.52.0 → 1.54.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/system-info/index.js +175 -74
- package/lib/system-info/linux-os-info.js +9 -7
- package/lib/system-info/utils.js +35 -0
- package/package.json +6 -6
package/lib/system-info/index.js
CHANGED
|
@@ -15,30 +15,42 @@
|
|
|
15
15
|
// @ts-check
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const cp = require('node:child_process');
|
|
19
|
+
const fs = require('node:fs/promises');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const util = require('node:util');
|
|
22
|
+
const v8 = require('node:v8');
|
|
23
|
+
const {
|
|
24
|
+
primordials: { StringPrototypeMatch, StringPrototypeTrim },
|
|
25
|
+
} = require('@contrast/common');
|
|
20
26
|
const { getCloudProviderMetadata } = require('./cloud-provider-metadata');
|
|
21
|
-
const { primordials: { StringPrototypeConcat, StringPrototypeMatch } } = require('@contrast/common');
|
|
22
27
|
const getLinuxOsInfo = require('./linux-os-info');
|
|
28
|
+
const { humanReadableBytes } = require('./utils');
|
|
23
29
|
|
|
24
30
|
const MOUNTINFO_REGEX = /\/docker\/containers\/(.*?)\//;
|
|
25
31
|
const CGROUP_REGEX = /:\/docker\/([^/]+)$/;
|
|
32
|
+
const MAX_CGROUP_MEMORY_LIMIT = 2 ** 63 - 1; // Common "unlimited" value for cgroup limits
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
const result = { used: !!process.env.pmx, version: null };
|
|
29
|
-
|
|
30
|
-
if (pkg?.dependences?.['pm2']) {
|
|
31
|
-
result.version = pkg.dependencies['pm2'];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return result;
|
|
35
|
-
}
|
|
34
|
+
const exec = util.promisify(cp.exec);
|
|
36
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Asynchronously determines if the current environment is running inside a
|
|
38
|
+
* Docker container.
|
|
39
|
+
*
|
|
40
|
+
* The function checks for Docker-specific indicators in the following order:
|
|
41
|
+
* 1. Parses `/proc/self/mountinfo` for Docker mount information.
|
|
42
|
+
* 2. Parses `/proc/self/cgroup` for Docker cgroup information.
|
|
43
|
+
* 3. Checks for the existence of the `/.dockerenv` file.
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<{ isDocker: boolean, containerId: string|null }>}
|
|
46
|
+
* An object indicating whether the environment is Docker and the container ID
|
|
47
|
+
* if available.
|
|
48
|
+
*/
|
|
37
49
|
async function getDockerInfo() {
|
|
38
50
|
try {
|
|
39
51
|
const result = await fs.readFile('/proc/self/mountinfo', 'utf8');
|
|
40
52
|
const matches = StringPrototypeMatch.call(result, MOUNTINFO_REGEX);
|
|
41
|
-
if (matches) return { isDocker: true,
|
|
53
|
+
if (matches) return { isDocker: true, containerId: matches[1] };
|
|
42
54
|
} catch (err) {
|
|
43
55
|
// else check /proc/self/cgroup
|
|
44
56
|
}
|
|
@@ -46,32 +58,115 @@ async function getDockerInfo() {
|
|
|
46
58
|
try {
|
|
47
59
|
const result = await fs.readFile('/proc/self/cgroup', 'utf8');
|
|
48
60
|
const matches = StringPrototypeMatch.call(result, CGROUP_REGEX);
|
|
49
|
-
if (matches) return { isDocker: true,
|
|
61
|
+
if (matches) return { isDocker: true, containerId: matches[1] };
|
|
50
62
|
} catch (err) {
|
|
51
63
|
// else check /.dockerenv
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
try {
|
|
55
67
|
const result = await fs.stat('/.dockerenv');
|
|
56
|
-
if (result) return { isDocker: true,
|
|
68
|
+
if (result) return { isDocker: true, containerId: null };
|
|
57
69
|
} catch (err) {
|
|
58
70
|
// if there's not such file we can conclude it's not docker env
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
return { isDocker: false,
|
|
73
|
+
return { isDocker: false, containerId: null };
|
|
62
74
|
}
|
|
63
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves information about whether the current environment is running inside
|
|
78
|
+
* Kubernetes.
|
|
79
|
+
*
|
|
80
|
+
* @returns {{ isKubernetes: boolean }} An object indicating if the environment
|
|
81
|
+
* is Kubernetes.
|
|
82
|
+
*/
|
|
64
83
|
function getKubernetesInfo() {
|
|
65
84
|
return { isKubernetes: !!process.env.KUBERNETES_SERVICE_HOST };
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Determines if the application is using PM2 and retrieves the PM2 version from
|
|
89
|
+
* the package dependencies.
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} pkg - The package.json object.
|
|
92
|
+
* @param {Object} [pkg.dependencies] - The dependencies listed in package.json.
|
|
93
|
+
* @returns {{ used: boolean, version: string|null }} An object indicating if PM2
|
|
94
|
+
* used and its version (if available).
|
|
95
|
+
*/
|
|
96
|
+
function isUsingPM2(pkg) {
|
|
97
|
+
const result = { used: !!process.env.pmx, version: null };
|
|
98
|
+
|
|
99
|
+
if (pkg?.dependencies?.['pm2']) {
|
|
100
|
+
result.version = pkg.dependencies['pm2'];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Asynchronously retrieves the Docker container's memory limit in bytes, if
|
|
108
|
+
* running inside a cgroup-limited environment.
|
|
109
|
+
*
|
|
110
|
+
* Reads the memory limit from the cgroup file system. If the limit is less than
|
|
111
|
+
* the maximum allowed by cgroups, it returns the value in bytes. If no
|
|
112
|
+
* limit is detected or an error occurs, returns `undefined`.
|
|
113
|
+
*
|
|
114
|
+
* @returns {Promise<number|undefined>} The memory limit in bytes or `undefined`
|
|
115
|
+
* if the limit is not determined.
|
|
116
|
+
*/
|
|
117
|
+
async function getDockerMemoryLimit() {
|
|
118
|
+
let limitInBytes = NaN;
|
|
119
|
+
|
|
120
|
+
// cgroup v2
|
|
121
|
+
try {
|
|
122
|
+
const result = await fs.readFile('/sys/fs/cgroup/memory.max', 'utf8');
|
|
123
|
+
limitInBytes = parseInt(StringPrototypeTrim.call(result), 10);
|
|
124
|
+
} catch {
|
|
125
|
+
// try v1...
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// cgroup v1
|
|
129
|
+
try {
|
|
130
|
+
const result = await fs.readFile('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
|
|
131
|
+
limitInBytes = parseInt(StringPrototypeTrim.call(result), 10);
|
|
132
|
+
} catch {
|
|
133
|
+
// no cgroup detected...
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!isNaN(limitInBytes) && limitInBytes < MAX_CGROUP_MEMORY_LIMIT) {
|
|
137
|
+
return limitInBytes;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retrieves macOS system information using the `sw_vers` command.
|
|
143
|
+
* Returns `null` if the command fails or is not available.
|
|
144
|
+
* @returns {Promise<{
|
|
145
|
+
* productName: string;
|
|
146
|
+
* productVersion: string;
|
|
147
|
+
* buildVersion: string;
|
|
148
|
+
* } | null>}
|
|
149
|
+
*/
|
|
150
|
+
async function macOsInfo() {
|
|
151
|
+
try {
|
|
152
|
+
return {
|
|
153
|
+
productName: StringPrototypeTrim.call((await exec('sw_vers -productName')).stdout),
|
|
154
|
+
productVersion: StringPrototypeTrim.call((await exec('sw_vers -productVersion')).stdout),
|
|
155
|
+
buildVersion: StringPrototypeTrim.call((await exec('sw_vers -buildVersion')).stdout)
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {import('..').Core & {
|
|
164
|
+
* _systemInfo: import('@contrast/common').SystemInfo | undefined;
|
|
165
|
+
* config: import('@contrast/config').Config;
|
|
166
|
+
* }} core
|
|
167
|
+
*/
|
|
168
|
+
module.exports = function (core) {
|
|
169
|
+
const { agentName, agentVersion, config, appInfo } = core;
|
|
75
170
|
|
|
76
171
|
// have values default to null so all required keys get serialized
|
|
77
172
|
core.getSystemInfo = async function getSystemInfo() {
|
|
@@ -80,77 +175,83 @@ module.exports = function(core) {
|
|
|
80
175
|
|
|
81
176
|
const cpus = os.cpus();
|
|
82
177
|
const totalmem = os.totalmem();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const osInfo = await getLinuxOsInfo();
|
|
87
|
-
if (osInfo && osInfo.file) {
|
|
88
|
-
linuxOsInfo = {
|
|
89
|
-
Id: osInfo.id,
|
|
90
|
-
VersionId: osInfo.version_id,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
178
|
+
const heapStats = v8.getHeapStatistics();
|
|
179
|
+
const free_heap_size = heapStats.heap_size_limit - heapStats.used_heap_size;
|
|
180
|
+
const dockerMemoryLimit = await getDockerMemoryLimit();
|
|
93
181
|
|
|
182
|
+
const osMemoryInfo = {
|
|
183
|
+
total: humanReadableBytes(totalmem),
|
|
184
|
+
totalBytes: totalmem,
|
|
185
|
+
};
|
|
186
|
+
const linuxOsInfo = await getLinuxOsInfo();
|
|
94
187
|
|
|
95
188
|
/** @type {import('@contrast/common').SystemInfo} */
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
189
|
+
const systemInfo = {
|
|
190
|
+
reportDate: new Date().toISOString(),
|
|
191
|
+
hostname: os.hostname(),
|
|
192
|
+
contrast: {
|
|
193
|
+
url: config.api.url ?? null,
|
|
194
|
+
proxy: {
|
|
102
195
|
enable: !!config.api.proxy.enable,
|
|
103
|
-
url: config.api.proxy.url
|
|
196
|
+
url: config.api.proxy.url ?? null,
|
|
104
197
|
},
|
|
105
|
-
|
|
106
|
-
|
|
198
|
+
server: {
|
|
199
|
+
name: config.server.name,
|
|
107
200
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
201
|
+
agent: {
|
|
202
|
+
name: agentName,
|
|
203
|
+
version: agentVersion,
|
|
111
204
|
},
|
|
112
205
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
206
|
+
node: {
|
|
207
|
+
path: process.execPath,
|
|
208
|
+
version: process.version,
|
|
209
|
+
memory: {
|
|
210
|
+
total: humanReadableBytes(heapStats.heap_size_limit),
|
|
211
|
+
totalBytes: heapStats.heap_size_limit,
|
|
212
|
+
used: humanReadableBytes(heapStats.used_heap_size),
|
|
213
|
+
usedBytes: heapStats.used_heap_size,
|
|
214
|
+
free: humanReadableBytes(free_heap_size),
|
|
215
|
+
freeBytes: free_heap_size,
|
|
216
|
+
},
|
|
116
217
|
},
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
218
|
+
os: {
|
|
219
|
+
architecture: os.arch(),
|
|
220
|
+
name: os.type(),
|
|
221
|
+
version: os.release(),
|
|
222
|
+
kernelVersion: os.version(),
|
|
223
|
+
cpu: {
|
|
224
|
+
type: cpus[0].model,
|
|
225
|
+
count: cpus.length,
|
|
125
226
|
},
|
|
126
|
-
|
|
127
|
-
|
|
227
|
+
memory: osMemoryInfo,
|
|
228
|
+
linuxInfo: linuxOsInfo?.file ? linuxOsInfo : null,
|
|
229
|
+
macOsInfo: await macOsInfo(),
|
|
128
230
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Used: StringPrototypeConcat.call(((totalmem - freemem) / 1e6).toFixed(0), ' MB'),
|
|
231
|
+
host: {
|
|
232
|
+
docker: await getDockerInfo(),
|
|
233
|
+
kubernetes: getKubernetesInfo(),
|
|
234
|
+
pm2: isUsingPM2(appInfo.pkg),
|
|
235
|
+
memory: {
|
|
236
|
+
total: dockerMemoryLimit ? humanReadableBytes(dockerMemoryLimit) : osMemoryInfo.total,
|
|
237
|
+
totalBytes: dockerMemoryLimit ?? osMemoryInfo.totalBytes,
|
|
137
238
|
},
|
|
138
239
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
240
|
+
application: appInfo.pkg,
|
|
241
|
+
cloud: {
|
|
242
|
+
provider: null,
|
|
243
|
+
resourceId: null,
|
|
244
|
+
},
|
|
144
245
|
};
|
|
145
246
|
|
|
146
247
|
if (config.server.discover_cloud_resource) {
|
|
147
248
|
const metadata = await getCloudProviderMetadata(config.inventory.gather_metadata_via);
|
|
148
249
|
if (metadata) {
|
|
149
|
-
|
|
150
|
-
|
|
250
|
+
systemInfo.cloud.provider = metadata.provider;
|
|
251
|
+
systemInfo.cloud.resourceId = metadata.id;
|
|
151
252
|
}
|
|
152
253
|
}
|
|
153
254
|
|
|
154
|
-
return core._systemInfo =
|
|
255
|
+
return (core._systemInfo = systemInfo);
|
|
155
256
|
};
|
|
156
257
|
};
|
|
@@ -60,16 +60,21 @@ function addEtcAlpineReleaseToOutputData(data, outputData) {
|
|
|
60
60
|
const defaultList = [
|
|
61
61
|
{ path: '/etc/os-release', parser: addOsReleaseToOutputData },
|
|
62
62
|
{ path: '/usr/lib/os-release', parser: addOsReleaseToOutputData },
|
|
63
|
-
{ path: '/etc/alpine-release', parser: addEtcAlpineReleaseToOutputData }
|
|
63
|
+
{ path: '/etc/alpine-release', parser: addEtcAlpineReleaseToOutputData },
|
|
64
64
|
];
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Get OS release info with information from '/etc/os-release', '/usr/lib/os-release',
|
|
68
68
|
* or '/etc/alpine-release'. The information in that file is distribution-dependent.
|
|
69
69
|
*
|
|
70
|
-
* @returns Promise<
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* @returns {Promise<{
|
|
71
|
+
* file: string;
|
|
72
|
+
* [key: string]: string;
|
|
73
|
+
* } | {
|
|
74
|
+
* file: undefined
|
|
75
|
+
* } | null>} where object is null if not Linux, or an object with the a file
|
|
76
|
+
* property and the key-value pairs from the file. Any quotes around the values
|
|
77
|
+
* are removed.
|
|
73
78
|
*
|
|
74
79
|
* the file property in the info object will be filled in with one of:
|
|
75
80
|
* - the file path (above) used
|
|
@@ -92,7 +97,6 @@ async function linuxOsInfo(opts = {}) {
|
|
|
92
97
|
list[i].parser(data, outputData);
|
|
93
98
|
|
|
94
99
|
return outputData;
|
|
95
|
-
|
|
96
100
|
} catch (e) {
|
|
97
101
|
i += 1;
|
|
98
102
|
continue;
|
|
@@ -103,7 +107,6 @@ async function linuxOsInfo(opts = {}) {
|
|
|
103
107
|
return { file: undefined };
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
|
|
107
110
|
function addOsReleaseToOutputData(data, outputData) {
|
|
108
111
|
const lines = data.split('\n');
|
|
109
112
|
|
|
@@ -131,4 +134,3 @@ function addOsReleaseToOutputData(data, outputData) {
|
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
module.exports = linuxOsInfo;
|
|
134
|
-
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2025 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 { primordials: { StringPrototypeConcat } } = require('@contrast/common');
|
|
19
|
+
const DEPTH_TO_PREFIX = ['', 'k', 'M', 'G', 'T']; // I don't think we're going past terabytes of memory...
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Converts a byte value into a human-readable string with appropriate units (kB, MB, GB, TB).
|
|
23
|
+
*
|
|
24
|
+
* @param {number} bytes - The number of bytes to convert.
|
|
25
|
+
* @param {number} [depth=0] - The current depth of conversion, used internally for recursion.
|
|
26
|
+
* @returns {string} The human-readable string representation of the byte value.
|
|
27
|
+
*/
|
|
28
|
+
module.exports.humanReadableBytes = function humanReadableBytes(bytes, depth = 0) {
|
|
29
|
+
if (bytes >= 1024 && depth < 4) {
|
|
30
|
+
return humanReadableBytes(bytes / 1024, depth + 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return StringPrototypeConcat.call(bytes.toFixed(depth > 0 ? 2 : 0), ' ', DEPTH_TO_PREFIX[depth], 'B');
|
|
34
|
+
};
|
|
35
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.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,15 +16,15 @@
|
|
|
16
16
|
"node": ">= 16.9.1"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "../scripts/test.sh"
|
|
19
|
+
"test": "bash ../scripts/test.sh"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@contrast/common": "1.
|
|
23
|
-
"@contrast/config": "1.
|
|
22
|
+
"@contrast/common": "1.34.0",
|
|
23
|
+
"@contrast/config": "1.49.0",
|
|
24
24
|
"@contrast/find-package-json": "^1.1.0",
|
|
25
25
|
"@contrast/fn-inspect": "^4.3.0",
|
|
26
|
-
"@contrast/logger": "1.
|
|
27
|
-
"@contrast/patcher": "1.
|
|
26
|
+
"@contrast/logger": "1.27.0",
|
|
27
|
+
"@contrast/patcher": "1.26.0",
|
|
28
28
|
"@contrast/perf": "1.3.1",
|
|
29
29
|
"@tsxper/crc32": "^2.1.3",
|
|
30
30
|
"axios": "^1.7.4",
|