@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.
@@ -15,30 +15,42 @@
15
15
  // @ts-check
16
16
  'use strict';
17
17
 
18
- const fs = require('fs/promises');
19
- const os = require('os');
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
- function isUsingPM2(pkg) {
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, containerID: matches[1] };
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, containerID: matches[1] };
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, containerID: null };
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, containerID: null };
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
- module.exports = function(core) {
69
- const {
70
- agentName,
71
- agentVersion,
72
- config,
73
- appInfo,
74
- } = core;
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 freemem = os.freemem();
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
- }
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 info = {
97
- ReportDate: new Date().toISOString(),
98
- MachineName: os.hostname(),
99
- Contrast: {
100
- Url: config.api.url || null,
101
- Proxy: {
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 || null,
196
+ url: config.api.proxy.url ?? null,
104
197
  },
105
- Server: {
106
- Name: config.server.name,
198
+ server: {
199
+ name: config.server.name,
107
200
  },
108
- Agent: {
109
- Name: agentName,
110
- Version: agentVersion,
201
+ agent: {
202
+ name: agentName,
203
+ version: agentVersion,
111
204
  },
112
205
  },
113
- Node: {
114
- Path: process.execPath,
115
- Version: process.version,
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
- OperatingSystem: {
118
- Architecture: os.arch(),
119
- Name: os.type(),
120
- Version: os.release(),
121
- KernelVersion: os.version(),
122
- CPU: {
123
- Type: cpus[0].model,
124
- Count: cpus.length,
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
- // Id, VersionId if linux, else null
127
- ...linuxOsInfo,
227
+ memory: osMemoryInfo,
228
+ linuxInfo: linuxOsInfo?.file ? linuxOsInfo : null,
229
+ macOsInfo: await macOsInfo(),
128
230
  },
129
- Host: {
130
- Docker: await getDockerInfo(),
131
- Kubernetes: getKubernetesInfo(),
132
- PM2: isUsingPM2(appInfo.pkg),
133
- Memory: {
134
- Total: StringPrototypeConcat.call((totalmem / 1e6).toFixed(0), ' MB'),
135
- Free: StringPrototypeConcat.call((freemem / 1e6).toFixed(0), ' MB'),
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
- Application: appInfo.pkg,
140
- Cloud: {
141
- Provider: null,
142
- ResourceID: null,
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
- info.Cloud.Provider = metadata.provider;
150
- info.Cloud.ResourceID = metadata.id;
250
+ systemInfo.cloud.provider = metadata.provider;
251
+ systemInfo.cloud.resourceId = metadata.id;
151
252
  }
152
253
  }
153
254
 
154
- return core._systemInfo = info;
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<Object> - where object is null if not Linux, or an object with
71
- * the a file property and the key-value pairs from the file. Any quotes around the
72
- * values are removed.
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.52.0",
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.32.0",
23
- "@contrast/config": "1.47.0",
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.25.0",
27
- "@contrast/patcher": "1.24.0",
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",