@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 +16 -0
- package/lib/client.js +57 -0
- package/lib/index.js +2 -1
- package/lib/packages.js +61 -0
- package/package.json +1 -1
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 };
|
package/lib/packages.js
ADDED
|
@@ -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 };
|