z-shield 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/cli.js +99 -15
- package/src/logger.js +3 -2
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,8 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const net = require('node:net');
|
|
7
7
|
const readline = require('node:readline');
|
|
8
8
|
|
|
9
|
-
const { stripAnsi, sanitizeString } = require('./logger');
|
|
10
|
-
const { parseDuration, isValidIp } = require('./block-manager');
|
|
9
|
+
const { stripAnsi, sanitizeString, sanitizeRoute, sanitizeMethod } = require('./logger');
|
|
10
|
+
const { parseDuration, isValidIp, normalizeIp } = require('./block-manager');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Parses raw command-line arguments into a structured command object.
|
|
@@ -284,6 +284,83 @@ function readLogs(logFilePath, limit = 20) {
|
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Sanitizes untrusted IP address values from logs for safe status aggregation and output.
|
|
289
|
+
* Strips ANSI escapes, truncates at newlines/control characters, and extracts valid IP addresses.
|
|
290
|
+
*
|
|
291
|
+
* @param {*} rawIp
|
|
292
|
+
* @returns {string} Normalized IP address or 'unknown'
|
|
293
|
+
*/
|
|
294
|
+
function sanitizeIp(rawIp) {
|
|
295
|
+
if (!rawIp || typeof rawIp !== 'string') return 'unknown';
|
|
296
|
+
const trimmed = rawIp.trim();
|
|
297
|
+
if (trimmed.length === 0) return 'unknown';
|
|
298
|
+
|
|
299
|
+
if (net.isIP(trimmed) !== 0) {
|
|
300
|
+
return normalizeIp(trimmed);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const stripped = stripAnsi(trimmed);
|
|
304
|
+
if (net.isIP(stripped) !== 0) {
|
|
305
|
+
return normalizeIp(stripped);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const beforeEscape = trimmed.split(/[\x1b\r\n\x00-\x1f\x7f]/)[0].trim();
|
|
309
|
+
if (beforeEscape.length > 0 && net.isIP(beforeEscape) !== 0) {
|
|
310
|
+
return normalizeIp(beforeEscape);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const ipv4Match = stripped.match(/(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/);
|
|
314
|
+
if (ipv4Match && net.isIP(ipv4Match[0]) === 4) {
|
|
315
|
+
return normalizeIp(ipv4Match[0]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ipv6Candidates = stripped.match(/[0-9a-fA-F:]+/g);
|
|
319
|
+
if (ipv6Candidates) {
|
|
320
|
+
for (const cand of ipv6Candidates) {
|
|
321
|
+
if (net.isIP(cand) === 6) {
|
|
322
|
+
return normalizeIp(cand);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return 'unknown';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Sanitizes untrusted event type strings from logs.
|
|
332
|
+
*
|
|
333
|
+
* @param {*} rawType
|
|
334
|
+
* @param {number} [maxLength=30]
|
|
335
|
+
* @returns {string} Clean event type string
|
|
336
|
+
*/
|
|
337
|
+
function sanitizeType(rawType, maxLength = 30) {
|
|
338
|
+
if (!rawType || typeof rawType !== 'string') return 'unknown';
|
|
339
|
+
let clean = rawType.split(/[\r\n\x1b]/)[0];
|
|
340
|
+
clean = stripAnsi(clean);
|
|
341
|
+
clean = clean.replace(/[\x00-\x1f\x7f]/g, ' ').trim();
|
|
342
|
+
clean = clean.replace(/\s+/g, ' ');
|
|
343
|
+
return clean.slice(0, maxLength) || 'unknown';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Sanitizes untrusted action strings from logs.
|
|
348
|
+
*
|
|
349
|
+
* @param {*} rawAction
|
|
350
|
+
* @param {number} [maxLength=20]
|
|
351
|
+
* @returns {string} Clean action string
|
|
352
|
+
*/
|
|
353
|
+
function sanitizeAction(rawAction, maxLength = 20) {
|
|
354
|
+
if (!rawAction || typeof rawAction !== 'string') return 'unknown';
|
|
355
|
+
let clean = rawAction.split(/[\r\n\x1b]/)[0];
|
|
356
|
+
clean = stripAnsi(clean);
|
|
357
|
+
clean = clean.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
|
|
358
|
+
if (clean === 'warn') clean = 'warned';
|
|
359
|
+
else if (clean === 'block') clean = 'blocked';
|
|
360
|
+
else if (clean === 'allow') clean = 'allowed';
|
|
361
|
+
return clean.slice(0, maxLength) || 'unknown';
|
|
362
|
+
}
|
|
363
|
+
|
|
287
364
|
/**
|
|
288
365
|
* Computes security summary statistics from the log file.
|
|
289
366
|
*
|
|
@@ -297,16 +374,16 @@ function calculateStatus(logFilePath) {
|
|
|
297
374
|
|
|
298
375
|
const { events: allEvents, malformedCount } = readLogs(logFilePath, 100000);
|
|
299
376
|
|
|
300
|
-
const ipCounts =
|
|
301
|
-
const routeCounts =
|
|
302
|
-
const typeCounts =
|
|
303
|
-
const actionCounts =
|
|
377
|
+
const ipCounts = Object.create(null);
|
|
378
|
+
const routeCounts = Object.create(null);
|
|
379
|
+
const typeCounts = Object.create(null);
|
|
380
|
+
const actionCounts = Object.create(null);
|
|
304
381
|
|
|
305
382
|
for (const event of allEvents) {
|
|
306
|
-
const ip = event.ip
|
|
307
|
-
const route = event.route
|
|
308
|
-
const type = event.type
|
|
309
|
-
const action = event.action
|
|
383
|
+
const ip = sanitizeIp(event.ip);
|
|
384
|
+
const route = sanitizeRoute(event.route);
|
|
385
|
+
const type = sanitizeType(event.type);
|
|
386
|
+
const action = sanitizeAction(event.action);
|
|
310
387
|
|
|
311
388
|
ipCounts[ip] = (ipCounts[ip] || 0) + 1;
|
|
312
389
|
routeCounts[route] = (routeCounts[route] || 0) + 1;
|
|
@@ -372,7 +449,8 @@ async function runCommand(argv = process.argv.slice(2), output = console) {
|
|
|
372
449
|
|
|
373
450
|
output.log(`\n${c.bold('Actions Breakdown:')}`);
|
|
374
451
|
for (const [action, count] of Object.entries(status.actionCounts)) {
|
|
375
|
-
const
|
|
452
|
+
const cleanAction = sanitizeAction(action);
|
|
453
|
+
const upper = cleanAction.toUpperCase();
|
|
376
454
|
let styledAction = upper;
|
|
377
455
|
if (upper === 'BLOCKED') styledAction = c.bold(c.red(upper));
|
|
378
456
|
else if (upper === 'WARNED') styledAction = c.bold(c.yellow(upper));
|
|
@@ -384,20 +462,23 @@ async function runCommand(argv = process.argv.slice(2), output = console) {
|
|
|
384
462
|
|
|
385
463
|
output.log(`\n${c.bold('Event Types:')}`);
|
|
386
464
|
for (const [type, count] of Object.entries(status.typeCounts)) {
|
|
387
|
-
|
|
465
|
+
const cleanType = sanitizeType(type);
|
|
466
|
+
output.log(` - ${c.yellow(cleanType)}: ${c.bold(count)}`);
|
|
388
467
|
}
|
|
389
468
|
|
|
390
469
|
if (status.topIps.length > 0) {
|
|
391
470
|
output.log(`\n${c.bold('Top Offending IPs:')}`);
|
|
392
471
|
for (const item of status.topIps) {
|
|
393
|
-
|
|
472
|
+
const cleanIp = sanitizeIp(item.ip);
|
|
473
|
+
output.log(` - ${c.cyan(cleanIp)}: ${c.bold(item.count)} events`);
|
|
394
474
|
}
|
|
395
475
|
}
|
|
396
476
|
|
|
397
477
|
if (status.topRoutes.length > 0) {
|
|
398
478
|
output.log(`\n${c.bold('Top Targeted Routes:')}`);
|
|
399
479
|
for (const item of status.topRoutes) {
|
|
400
|
-
|
|
480
|
+
const cleanRoute = sanitizeRoute(item.route);
|
|
481
|
+
output.log(` - ${c.underline(cleanRoute)}: ${c.bold(item.count)} events`);
|
|
401
482
|
}
|
|
402
483
|
}
|
|
403
484
|
|
|
@@ -676,5 +757,8 @@ module.exports = {
|
|
|
676
757
|
readLogs,
|
|
677
758
|
calculateStatus,
|
|
678
759
|
resolveLogPath,
|
|
679
|
-
runCommand
|
|
760
|
+
runCommand,
|
|
761
|
+
sanitizeIp,
|
|
762
|
+
sanitizeType,
|
|
763
|
+
sanitizeAction
|
|
680
764
|
};
|
package/src/logger.js
CHANGED
|
@@ -46,11 +46,12 @@ function sanitizeRoute(route, maxLength = 500) {
|
|
|
46
46
|
if (!route || typeof route !== 'string') {
|
|
47
47
|
return '/';
|
|
48
48
|
}
|
|
49
|
-
let clean =
|
|
49
|
+
let clean = route.split(/[\r\n]/)[0];
|
|
50
|
+
clean = stripAnsi(clean);
|
|
50
51
|
// Strip query parameters and URL fragments to prevent credential leakage (CWE-532)
|
|
51
52
|
clean = clean.split('?')[0].split('#')[0];
|
|
52
53
|
// Remove dangerous control characters and newlines
|
|
53
|
-
clean = clean.replace(/[\x00-\x1f\x7f]/g, '');
|
|
54
|
+
clean = clean.replace(/[\x00-\x1f\x7f]/g, '').trim();
|
|
54
55
|
if (!clean.startsWith('/')) {
|
|
55
56
|
clean = '/' + clean;
|
|
56
57
|
}
|