@contrast/core 1.51.0 → 1.53.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 +144 -75
- package/lib/system-info/linux-os-info.js +4 -1
- package/lib/system-info/utils.js +35 -0
- package/package.json +6 -6
package/lib/system-info/index.js
CHANGED
|
@@ -17,28 +17,36 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs/promises');
|
|
19
19
|
const os = require('os');
|
|
20
|
+
const v8 = require('v8');
|
|
21
|
+
const {
|
|
22
|
+
primordials: { StringPrototypeMatch, StringPrototypeTrim },
|
|
23
|
+
} = require('@contrast/common');
|
|
20
24
|
const { getCloudProviderMetadata } = require('./cloud-provider-metadata');
|
|
21
|
-
const { primordials: { StringPrototypeConcat, StringPrototypeMatch } } = require('@contrast/common');
|
|
22
25
|
const getLinuxOsInfo = require('./linux-os-info');
|
|
26
|
+
const { humanReadableBytes } = require('./utils');
|
|
23
27
|
|
|
24
28
|
const MOUNTINFO_REGEX = /\/docker\/containers\/(.*?)\//;
|
|
25
29
|
const CGROUP_REGEX = /:\/docker\/([^/]+)$/;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const MAX_CGROUP_MEMORY_LIMIT = 2 ** 63 - 1; // Common "unlimited" value for cgroup limits
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Asynchronously determines if the current environment is running inside a
|
|
34
|
+
* Docker container.
|
|
35
|
+
*
|
|
36
|
+
* The function checks for Docker-specific indicators in the following order:
|
|
37
|
+
* 1. Parses `/proc/self/mountinfo` for Docker mount information.
|
|
38
|
+
* 2. Parses `/proc/self/cgroup` for Docker cgroup information.
|
|
39
|
+
* 3. Checks for the existence of the `/.dockerenv` file.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<{ isDocker: boolean, containerId: string|null }>}
|
|
42
|
+
* An object indicating whether the environment is Docker and the container ID
|
|
43
|
+
* if available.
|
|
44
|
+
*/
|
|
37
45
|
async function getDockerInfo() {
|
|
38
46
|
try {
|
|
39
47
|
const result = await fs.readFile('/proc/self/mountinfo', 'utf8');
|
|
40
48
|
const matches = StringPrototypeMatch.call(result, MOUNTINFO_REGEX);
|
|
41
|
-
if (matches) return { isDocker: true,
|
|
49
|
+
if (matches) return { isDocker: true, containerId: matches[1] };
|
|
42
50
|
} catch (err) {
|
|
43
51
|
// else check /proc/self/cgroup
|
|
44
52
|
}
|
|
@@ -46,32 +54,94 @@ async function getDockerInfo() {
|
|
|
46
54
|
try {
|
|
47
55
|
const result = await fs.readFile('/proc/self/cgroup', 'utf8');
|
|
48
56
|
const matches = StringPrototypeMatch.call(result, CGROUP_REGEX);
|
|
49
|
-
if (matches) return { isDocker: true,
|
|
57
|
+
if (matches) return { isDocker: true, containerId: matches[1] };
|
|
50
58
|
} catch (err) {
|
|
51
59
|
// else check /.dockerenv
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
try {
|
|
55
63
|
const result = await fs.stat('/.dockerenv');
|
|
56
|
-
if (result) return { isDocker: true,
|
|
64
|
+
if (result) return { isDocker: true, containerId: null };
|
|
57
65
|
} catch (err) {
|
|
58
66
|
// if there's not such file we can conclude it's not docker env
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
return { isDocker: false,
|
|
69
|
+
return { isDocker: false, containerId: null };
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves information about whether the current environment is running inside
|
|
74
|
+
* Kubernetes.
|
|
75
|
+
*
|
|
76
|
+
* @returns {{ isKubernetes: boolean }} An object indicating if the environment
|
|
77
|
+
* is Kubernetes.
|
|
78
|
+
*/
|
|
64
79
|
function getKubernetesInfo() {
|
|
65
80
|
return { isKubernetes: !!process.env.KUBERNETES_SERVICE_HOST };
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Determines if the application is using PM2 and retrieves the PM2 version from
|
|
85
|
+
* the package dependencies.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} pkg - The package.json object.
|
|
88
|
+
* @param {Object} [pkg.dependencies] - The dependencies listed in package.json.
|
|
89
|
+
* @returns {{ used: boolean, version: string|null }} An object indicating if PM2
|
|
90
|
+
* used and its version (if available).
|
|
91
|
+
*/
|
|
92
|
+
function isUsingPM2(pkg) {
|
|
93
|
+
const result = { used: !!process.env.pmx, version: null };
|
|
94
|
+
|
|
95
|
+
if (pkg?.dependencies?.['pm2']) {
|
|
96
|
+
result.version = pkg.dependencies['pm2'];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Asynchronously retrieves the Docker container's memory limit in bytes, if
|
|
104
|
+
* running inside a cgroup-limited environment.
|
|
105
|
+
*
|
|
106
|
+
* Reads the memory limit from the cgroup file system. If the limit is less than
|
|
107
|
+
* the maximum allowed by cgroups, it returns the value in bytes. If no
|
|
108
|
+
* limit is detected or an error occurs, returns `undefined`.
|
|
109
|
+
*
|
|
110
|
+
* @returns {Promise<number|undefined>} The memory limit in bytes or `undefined`
|
|
111
|
+
* if the limit is not determined.
|
|
112
|
+
*/
|
|
113
|
+
async function getDockerMemoryLimit() {
|
|
114
|
+
let limitInBytes = NaN;
|
|
115
|
+
|
|
116
|
+
// cgroup v2
|
|
117
|
+
try {
|
|
118
|
+
const result = await fs.readFile('/sys/fs/cgroup/memory.max', 'utf8');
|
|
119
|
+
limitInBytes = parseInt(StringPrototypeTrim.call(result), 10);
|
|
120
|
+
} catch {
|
|
121
|
+
// try v1...
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// cgroup v1
|
|
125
|
+
try {
|
|
126
|
+
const result = await fs.readFile('/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8');
|
|
127
|
+
limitInBytes = parseInt(StringPrototypeTrim.call(result), 10);
|
|
128
|
+
} catch {
|
|
129
|
+
// no cgroup detected...
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!isNaN(limitInBytes) && limitInBytes < MAX_CGROUP_MEMORY_LIMIT) {
|
|
133
|
+
return limitInBytes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {import('..').Core & {
|
|
139
|
+
* _systemInfo: import('@contrast/common').SystemInfo | undefined;
|
|
140
|
+
* config: import('@contrast/config').Config;
|
|
141
|
+
* }} core
|
|
142
|
+
*/
|
|
143
|
+
module.exports = function (core) {
|
|
144
|
+
const { agentName, agentVersion, config, appInfo } = core;
|
|
75
145
|
|
|
76
146
|
// have values default to null so all required keys get serialized
|
|
77
147
|
core.getSystemInfo = async function getSystemInfo() {
|
|
@@ -79,78 +149,77 @@ module.exports = function(core) {
|
|
|
79
149
|
if (core._systemInfo) return core._systemInfo;
|
|
80
150
|
|
|
81
151
|
const cpus = os.cpus();
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
let linuxOsInfo = undefined;
|
|
86
|
-
const osInfo = await getLinuxOsInfo();
|
|
87
|
-
if (osInfo && osInfo.file) {
|
|
88
|
-
linuxOsInfo = {
|
|
89
|
-
Id: osInfo.id,
|
|
90
|
-
VersionId: osInfo.version_id,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
152
|
+
const heapStats = v8.getHeapStatistics();
|
|
153
|
+
const dockerMemoryLimit = await getDockerMemoryLimit();
|
|
93
154
|
|
|
155
|
+
const osMemoryInfo = {
|
|
156
|
+
total: humanReadableBytes(os.totalmem()),
|
|
157
|
+
};
|
|
158
|
+
const linuxOsInfo = await getLinuxOsInfo();
|
|
94
159
|
|
|
95
160
|
/** @type {import('@contrast/common').SystemInfo} */
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
161
|
+
const systemInfo = {
|
|
162
|
+
reportDate: new Date().toISOString(),
|
|
163
|
+
hostname: os.hostname(),
|
|
164
|
+
contrast: {
|
|
165
|
+
url: config.api.url ?? null,
|
|
166
|
+
proxy: {
|
|
102
167
|
enable: !!config.api.proxy.enable,
|
|
103
|
-
url: config.api.proxy.url
|
|
168
|
+
url: config.api.proxy.url ?? null,
|
|
104
169
|
},
|
|
105
|
-
|
|
106
|
-
|
|
170
|
+
server: {
|
|
171
|
+
name: config.server.name,
|
|
107
172
|
},
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
agent: {
|
|
174
|
+
name: agentName,
|
|
175
|
+
version: agentVersion,
|
|
111
176
|
},
|
|
112
177
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
node: {
|
|
179
|
+
path: process.execPath,
|
|
180
|
+
version: process.version,
|
|
181
|
+
memory: {
|
|
182
|
+
total: humanReadableBytes(heapStats.heap_size_limit),
|
|
183
|
+
used: humanReadableBytes(heapStats.used_heap_size),
|
|
184
|
+
free: humanReadableBytes(heapStats.heap_size_limit - heapStats.used_heap_size),
|
|
185
|
+
},
|
|
116
186
|
},
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
187
|
+
os: {
|
|
188
|
+
architecture: os.arch(),
|
|
189
|
+
name: os.type(),
|
|
190
|
+
version: os.release(),
|
|
191
|
+
kernelVersion: os.version(),
|
|
192
|
+
cpu: {
|
|
193
|
+
type: cpus[0].model,
|
|
194
|
+
count: cpus.length,
|
|
125
195
|
},
|
|
126
|
-
|
|
127
|
-
|
|
196
|
+
memory: osMemoryInfo,
|
|
197
|
+
id: linuxOsInfo?.file ? linuxOsInfo.id : undefined,
|
|
198
|
+
versionId: linuxOsInfo?.file ? linuxOsInfo.version_id : undefined,
|
|
128
199
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Free: StringPrototypeConcat.call((freemem / 1e6).toFixed(0), ' MB'),
|
|
136
|
-
Used: StringPrototypeConcat.call(((totalmem - freemem) / 1e6).toFixed(0), ' MB'),
|
|
200
|
+
host: {
|
|
201
|
+
docker: await getDockerInfo(),
|
|
202
|
+
kubernetes: getKubernetesInfo(),
|
|
203
|
+
pm2: isUsingPM2(appInfo.pkg),
|
|
204
|
+
memory: {
|
|
205
|
+
total: dockerMemoryLimit ? humanReadableBytes(dockerMemoryLimit) : osMemoryInfo.total,
|
|
137
206
|
},
|
|
138
207
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
208
|
+
application: appInfo.pkg,
|
|
209
|
+
cloud: {
|
|
210
|
+
provider: null,
|
|
211
|
+
resourceId: null,
|
|
212
|
+
},
|
|
144
213
|
};
|
|
145
214
|
|
|
146
215
|
if (config.server.discover_cloud_resource) {
|
|
147
216
|
const metadata = await getCloudProviderMetadata(config.inventory.gather_metadata_via);
|
|
148
217
|
if (metadata) {
|
|
149
|
-
|
|
150
|
-
|
|
218
|
+
systemInfo.cloud.provider = metadata.provider;
|
|
219
|
+
systemInfo.cloud.resourceId = metadata.id;
|
|
151
220
|
}
|
|
152
221
|
}
|
|
153
222
|
|
|
154
|
-
return core._systemInfo =
|
|
223
|
+
return (core._systemInfo = systemInfo);
|
|
155
224
|
};
|
|
156
225
|
};
|
|
@@ -67,7 +67,10 @@ const defaultList = [
|
|
|
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<
|
|
70
|
+
* @returns {Promise<{
|
|
71
|
+
* file?: string;
|
|
72
|
+
* [key: string]: string;
|
|
73
|
+
* }>} - where object is null if not Linux, or an object with
|
|
71
74
|
* the a file property and the key-value pairs from the file. Any quotes around the
|
|
72
75
|
* values are removed.
|
|
73
76
|
*
|
|
@@ -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.53.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.33.0",
|
|
23
|
+
"@contrast/config": "1.48.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.26.0",
|
|
27
|
+
"@contrast/patcher": "1.25.0",
|
|
28
28
|
"@contrast/perf": "1.3.1",
|
|
29
29
|
"@tsxper/crc32": "^2.1.3",
|
|
30
30
|
"axios": "^1.7.4",
|