@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.
@@ -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
- 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
- }
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, containerID: matches[1] };
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, containerID: matches[1] };
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, containerID: null };
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, containerID: null };
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
- module.exports = function(core) {
69
- const {
70
- agentName,
71
- agentVersion,
72
- config,
73
- appInfo,
74
- } = core;
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 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
- }
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 info = {
97
- ReportDate: new Date().toISOString(),
98
- MachineName: os.hostname(),
99
- Contrast: {
100
- Url: config.api.url || null,
101
- Proxy: {
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 || null,
168
+ url: config.api.proxy.url ?? null,
104
169
  },
105
- Server: {
106
- Name: config.server.name,
170
+ server: {
171
+ name: config.server.name,
107
172
  },
108
- Agent: {
109
- Name: agentName,
110
- Version: agentVersion,
173
+ agent: {
174
+ name: agentName,
175
+ version: agentVersion,
111
176
  },
112
177
  },
113
- Node: {
114
- Path: process.execPath,
115
- Version: process.version,
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
- 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,
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
- // Id, VersionId if linux, else null
127
- ...linuxOsInfo,
196
+ memory: osMemoryInfo,
197
+ id: linuxOsInfo?.file ? linuxOsInfo.id : undefined,
198
+ versionId: linuxOsInfo?.file ? linuxOsInfo.version_id : undefined,
128
199
  },
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'),
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
- Application: appInfo.pkg,
140
- Cloud: {
141
- Provider: null,
142
- ResourceID: null,
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
- info.Cloud.Provider = metadata.provider;
150
- info.Cloud.ResourceID = metadata.id;
218
+ systemInfo.cloud.provider = metadata.provider;
219
+ systemInfo.cloud.resourceId = metadata.id;
151
220
  }
152
221
  }
153
222
 
154
- return core._systemInfo = info;
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<Object> - where object is null if not Linux, or an object with
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.51.0",
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.32.0",
23
- "@contrast/config": "1.46.0",
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.24.0",
27
- "@contrast/patcher": "1.23.0",
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",