@webimpian/node-log-central 1.0.2 → 1.1.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/README.md CHANGED
@@ -64,6 +64,22 @@ process.on('SIGTERM', () => logs.close());
64
64
 
65
65
  Delivery semantics: batches that fail with a network error or 5xx stay queued and retry with capped backoff (max 60s); batches rejected with a 4xx are dropped (retrying cannot fix them) and reported to `onError`.
66
66
 
67
+ ## Vulnerable packages
68
+
69
+ Call `reportPackages()` once at startup. It sends the name and version of every package in `package-lock.json` to Log Central now, and again once a day. Log Central checks them against known vulnerabilities and alerts on critical, high or actively exploited ones. Only names and versions leave the app.
70
+
71
+ ```
72
+ logs.reportPackages();
73
+ ```
74
+
75
+ | Option | Default | Purpose |
76
+ |---|---|---|
77
+ | `dir` | `process.cwd()` | Folder that holds `package-lock.json` |
78
+ | `name` | the app's slug | Name shown in Log Central, for an app with more than one lockfile |
79
+ | `interval` | 24 hours | Time between reports, in ms |
80
+
81
+ It returns a promise for `true` when Log Central accepted the list and never throws; failures go to `onError`. `logs.close()` stops the daily repeat. Needs Log Central v1.16.4 or newer.
82
+
67
83
  ## Testing
68
84
 
69
85
  ```
package/lib/client.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const os = require('node:os');
4
4
  const crypto = require('node:crypto');
5
+ const { collectPackages } = require('./packages');
5
6
 
6
7
  const LEVELS = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
7
8
 
@@ -17,6 +18,8 @@ const ENTRYPOINTS = ['http', 'console', 'queue'];
17
18
 
18
19
  const MAX_BATCH = 1000;
19
20
 
21
+ const DAY = 24 * 60 * 60 * 1000;
22
+
20
23
  function formatTimestamp(date = new Date()) {
21
24
  const pad = (value, width = 2) => String(value).padStart(width, '0');
22
25
 
@@ -95,6 +98,7 @@ class LogCentralClient {
95
98
  this.failures = 0;
96
99
  this.retryAt = 0;
97
100
  this.flushing = null;
101
+ this.packagesTimer = null;
98
102
 
99
103
  this.timer = setInterval(() => {
100
104
  this.flush().catch(() => {});
@@ -205,8 +209,61 @@ class LogCentralClient {
205
209
  return this.flushing;
206
210
  }
207
211
 
212
+ // Sends the installed npm package versions for vulnerability checks, now and then once a day.
213
+ reportPackages(options = {}) {
214
+ const send = () => this.#sendPackages(options).catch(() => false);
215
+
216
+ clearInterval(this.packagesTimer);
217
+ this.packagesTimer = setInterval(send, options.interval || DAY);
218
+
219
+ if (typeof this.packagesTimer.unref === 'function') {
220
+ this.packagesTimer.unref();
221
+ }
222
+
223
+ return send();
224
+ }
225
+
226
+ async #sendPackages(options) {
227
+ const inventory = {
228
+ kind: 'node',
229
+ hostname: this.hostname,
230
+ runtime: { node: process.versions.node },
231
+ packages: collectPackages(options.dir),
232
+ };
233
+
234
+ if (options.name) {
235
+ inventory.name = options.name;
236
+ }
237
+
238
+ try {
239
+ const response = await this.fetch(`${this.url}/packages`, {
240
+ method: 'POST',
241
+ headers: {
242
+ authorization: `Bearer ${this.token}`,
243
+ 'content-type': 'application/json',
244
+ },
245
+ body: JSON.stringify(inventory),
246
+ signal: AbortSignal.timeout(this.requestTimeout),
247
+ });
248
+
249
+ if (response.status >= 400) {
250
+ const body = await response.text().catch(() => '');
251
+ this.#report(new Error(`Log Central rejected the package list (${response.status}): ${body.slice(0, 200)}`));
252
+
253
+ return false;
254
+ }
255
+
256
+ return true;
257
+ } catch (err) {
258
+ this.#report(err);
259
+
260
+ return false;
261
+ }
262
+ }
263
+
208
264
  async close() {
209
265
  clearInterval(this.timer);
266
+ clearInterval(this.packagesTimer);
210
267
  this.retryAt = 0;
211
268
  await this.flush();
212
269
  }
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const { LogCentralClient, formatTimestamp, safeStringify, LEVELS } = require('./client');
4
+ const { collectPackages } = require('./packages');
4
5
 
5
- module.exports = { LogCentralClient, formatTimestamp, safeStringify, LEVELS };
6
+ module.exports = { LogCentralClient, formatTimestamp, safeStringify, LEVELS, collectPackages };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const MAX_PACKAGES = 10000;
7
+
8
+ function fromLockfileV1(dependencies, found) {
9
+ for (const [name, entry] of Object.entries(dependencies || {})) {
10
+ if (!entry || typeof entry.version !== 'string') {
11
+ continue;
12
+ }
13
+
14
+ found.push({ ecosystem: 'npm', name, version: entry.version, dev: entry.dev === true });
15
+ fromLockfileV1(entry.dependencies, found);
16
+ }
17
+ }
18
+
19
+ function collectPackages(dir = process.cwd()) {
20
+ let lock;
21
+
22
+ try {
23
+ lock = JSON.parse(fs.readFileSync(path.join(dir, 'package-lock.json'), 'utf8'));
24
+ } catch {
25
+ return [];
26
+ }
27
+
28
+ const found = [];
29
+
30
+ if (lock && typeof lock.packages === 'object' && lock.packages !== null) {
31
+ for (const [location, entry] of Object.entries(lock.packages)) {
32
+ if (location === '' || !entry || typeof entry.version !== 'string' || entry.link === true) {
33
+ continue;
34
+ }
35
+
36
+ const at = location.lastIndexOf('node_modules/');
37
+
38
+ found.push({
39
+ ecosystem: 'npm',
40
+ name: entry.name || (at === -1 ? location : location.slice(at + 13)),
41
+ version: entry.version,
42
+ dev: entry.dev === true,
43
+ });
44
+ }
45
+ } else {
46
+ // Lockfile v1 has no "packages" map, only nested "dependencies".
47
+ fromLockfileV1(lock && lock.dependencies, found);
48
+ }
49
+
50
+ const seen = new Set();
51
+
52
+ return found
53
+ .filter(entry => {
54
+ const key = `${entry.name}@${entry.version}|${entry.dev}`;
55
+
56
+ return seen.has(key) ? false : seen.add(key);
57
+ })
58
+ .slice(0, MAX_PACKAGES);
59
+ }
60
+
61
+ module.exports = { collectPackages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webimpian/node-log-central",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Ship logs and errors from Node.js applications to a Log Central server.",
5
5
  "keywords": [
6
6
  "logging",