z-shield 0.1.0 → 0.1.2

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.
Files changed (52) hide show
  1. package/README.md +123 -6
  2. package/dist/block-manager.d.ts +134 -0
  3. package/dist/block-manager.d.ts.map +1 -0
  4. package/dist/block-manager.js +425 -0
  5. package/dist/block-manager.js.map +1 -0
  6. package/dist/cli.d.ts +141 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +784 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +15 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +148 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/detector.d.ts +56 -0
  15. package/dist/detector.d.ts.map +1 -0
  16. package/dist/detector.js +81 -0
  17. package/dist/detector.js.map +1 -0
  18. package/dist/index.d.ts +29 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +141 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +347 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/middleware.d.ts +68 -0
  27. package/dist/middleware.d.ts.map +1 -0
  28. package/dist/middleware.js +272 -0
  29. package/dist/middleware.js.map +1 -0
  30. package/dist/next.d.ts +73 -0
  31. package/dist/next.d.ts.map +1 -0
  32. package/dist/next.js +486 -0
  33. package/dist/next.js.map +1 -0
  34. package/dist/rate-limiter.d.ts +75 -0
  35. package/dist/rate-limiter.d.ts.map +1 -0
  36. package/dist/rate-limiter.js +153 -0
  37. package/dist/rate-limiter.js.map +1 -0
  38. package/dist/types.d.ts +199 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +6 -0
  41. package/dist/types.js.map +1 -0
  42. package/next.d.ts +1 -0
  43. package/next.js +2 -0
  44. package/package.json +34 -5
  45. package/src/block-manager.js +0 -326
  46. package/src/cli.js +0 -680
  47. package/src/config.js +0 -165
  48. package/src/detector.js +0 -88
  49. package/src/index.js +0 -97
  50. package/src/logger.js +0 -279
  51. package/src/middleware.js +0 -288
  52. package/src/rate-limiter.js +0 -186
package/dist/cli.js ADDED
@@ -0,0 +1,784 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.parseCliArgs = parseCliArgs;
8
+ exports.resolveLogPath = resolveLogPath;
9
+ exports.createColors = createColors;
10
+ exports.resolveColorSupport = resolveColorSupport;
11
+ exports.formatLogEvent = formatLogEvent;
12
+ exports.readLogs = readLogs;
13
+ exports.sanitizeIp = sanitizeIp;
14
+ exports.sanitizeType = sanitizeType;
15
+ exports.sanitizeAction = sanitizeAction;
16
+ exports.calculateStatus = calculateStatus;
17
+ exports.runCommand = runCommand;
18
+ const node_fs_1 = __importDefault(require("node:fs"));
19
+ const node_path_1 = __importDefault(require("node:path"));
20
+ const node_net_1 = __importDefault(require("node:net"));
21
+ const logger_1 = require("./logger");
22
+ const block_manager_1 = require("./block-manager");
23
+ /**
24
+ * Parses raw command-line arguments into a structured command object.
25
+ *
26
+ * @param argv - Array of CLI arguments (typically process.argv.slice(2))
27
+ * @returns Structured CLI command, positional args, and parsed options
28
+ */
29
+ function parseCliArgs(argv = []) {
30
+ let command = 'help';
31
+ const args = [];
32
+ const options = {};
33
+ let i = 0;
34
+ // If first non-flag argument exists, it's the command
35
+ while (i < argv.length) {
36
+ const token = argv[i];
37
+ if (!token) {
38
+ i += 1;
39
+ continue;
40
+ }
41
+ if (token.startsWith('--') || token.startsWith('-')) {
42
+ const cleanKey = token.replace(/^-+/, '');
43
+ const nextToken = argv[i + 1];
44
+ // Check if next token is value
45
+ if (nextToken !== undefined && !nextToken.startsWith('-')) {
46
+ options[cleanKey] = nextToken;
47
+ i += 2;
48
+ }
49
+ else {
50
+ options[cleanKey] = true;
51
+ i += 1;
52
+ }
53
+ }
54
+ else {
55
+ if (command === 'help') {
56
+ command = token.toLowerCase();
57
+ }
58
+ else {
59
+ args.push(token);
60
+ }
61
+ i += 1;
62
+ }
63
+ }
64
+ if (options.help || options.h) {
65
+ command = 'help';
66
+ }
67
+ return { command, args, options };
68
+ }
69
+ /**
70
+ * Resolves the target JSON Lines log file path based on CLI options or defaults.
71
+ *
72
+ * @param options - Parsed CLI options
73
+ * @returns Absolute path to events.jsonl
74
+ */
75
+ function resolveLogPath(options = {}) {
76
+ const logDirOption = (options['log-dir'] || options.logDir);
77
+ const envLogDir = process.env.Z_SHIELD_LOG_DIR;
78
+ const filenameOption = (options['log-file'] || options.logFile);
79
+ // Precedence:
80
+ // 1. Explicit CLI option: --log-dir / --logDir
81
+ // 2. Environment variable: Z_SHIELD_LOG_DIR (if non-empty)
82
+ // 3. Default: .z-shield-logs in current working directory
83
+ let targetDir;
84
+ if (typeof logDirOption === 'string' && logDirOption.trim().length > 0) {
85
+ targetDir = logDirOption.trim();
86
+ }
87
+ else if (typeof envLogDir === 'string' && envLogDir.trim().length > 0) {
88
+ targetDir = envLogDir.trim();
89
+ }
90
+ else {
91
+ targetDir = node_path_1.default.join(process.cwd(), '.z-shield-logs');
92
+ }
93
+ const logDir = node_path_1.default.resolve(targetDir);
94
+ let filename = 'events.jsonl';
95
+ if (typeof filenameOption === 'string' && filenameOption.trim().length > 0) {
96
+ const candidate = filenameOption.trim();
97
+ if (!/^[a-zA-Z0-9._-]+$/.test(candidate) ||
98
+ candidate.includes('..') ||
99
+ candidate.includes('/') ||
100
+ candidate.includes('\\')) {
101
+ throw new Error('[Z-Shield CLI] "--log-file" must be a safe filename without path traversal characters.');
102
+ }
103
+ filename = candidate;
104
+ }
105
+ return node_path_1.default.join(logDir, filename);
106
+ }
107
+ /**
108
+ * Creates an ANSI color styling helper object.
109
+ * When enabled is false, all styling methods act as plain passthroughs.
110
+ *
111
+ * @param enabled - Whether color styling is active
112
+ * @returns Color styling map
113
+ */
114
+ function createColors(enabled = true) {
115
+ if (!enabled) {
116
+ const pass = (str) => String(str ?? '');
117
+ return {
118
+ bold: pass,
119
+ dim: pass,
120
+ italic: pass,
121
+ underline: pass,
122
+ inverse: pass,
123
+ red: pass,
124
+ green: pass,
125
+ yellow: pass,
126
+ blue: pass,
127
+ magenta: pass,
128
+ cyan: pass,
129
+ white: pass,
130
+ gray: pass,
131
+ enabled: false,
132
+ hasColors: false
133
+ };
134
+ }
135
+ const wrap = (open, close) => (str) => `\x1b[${open}m${str ?? ''}\x1b[${close}m`;
136
+ return {
137
+ bold: wrap(1, 22),
138
+ dim: wrap(2, 22),
139
+ italic: wrap(3, 23),
140
+ underline: wrap(4, 24),
141
+ inverse: wrap(7, 27),
142
+ red: wrap(31, 39),
143
+ green: wrap(32, 39),
144
+ yellow: wrap(33, 39),
145
+ blue: wrap(34, 39),
146
+ magenta: wrap(35, 39),
147
+ cyan: wrap(36, 39),
148
+ white: wrap(37, 39),
149
+ gray: wrap(90, 39),
150
+ enabled: true,
151
+ hasColors: true
152
+ };
153
+ }
154
+ /**
155
+ * Determines whether terminal colors should be enabled based on
156
+ * explicit options, environment variables, and TTY capabilities.
157
+ *
158
+ * Respects the standard NO_COLOR specification (https://no-color.org).
159
+ *
160
+ * @param options - Parsed CLI options
161
+ * @param output - Output destination (console or mock stream)
162
+ * @returns True if colors should be enabled
163
+ */
164
+ function resolveColorSupport(options = {}, output = null) {
165
+ // 1. Explicit CLI flags
166
+ if (options['no-color'] ||
167
+ options.noColor ||
168
+ options.color === false ||
169
+ options.colors === false ||
170
+ options.color === 'false' ||
171
+ options.colors === 'false') {
172
+ return false;
173
+ }
174
+ if (options.color === true ||
175
+ options.colors === true ||
176
+ options.color === 'true' ||
177
+ options.colors === 'true' ||
178
+ options.color === '') {
179
+ return true;
180
+ }
181
+ // 2. NO_COLOR standard (https://no-color.org)
182
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
183
+ return false;
184
+ }
185
+ // 3. Node.js NODE_DISABLE_COLORS
186
+ if (process.env.NODE_DISABLE_COLORS === '1') {
187
+ return false;
188
+ }
189
+ // 4. FORCE_COLOR override
190
+ if (process.env.FORCE_COLOR !== undefined) {
191
+ return process.env.FORCE_COLOR !== '0';
192
+ }
193
+ // 5. If output is a custom test mock or stream without TTY, disable colors by default
194
+ if (output && output !== console) {
195
+ return Boolean(output.isTTY);
196
+ }
197
+ // 6. Interactive terminal check
198
+ return Boolean(process.stdout && process.stdout.isTTY);
199
+ }
200
+ /**
201
+ * Formats a single security event record into a clean, sanitized terminal line.
202
+ * Strips all ANSI escapes and control characters from untrusted log fields before formatting.
203
+ *
204
+ * @param event - Security event object
205
+ * @param options - Options object or boolean for color
206
+ * @returns Sanitized output line
207
+ */
208
+ function formatLogEvent(event, options = {}) {
209
+ if (!event || typeof event !== 'object') {
210
+ return '[Malformed Event]';
211
+ }
212
+ let useColors = false;
213
+ if (typeof options === 'boolean') {
214
+ useColors = options;
215
+ }
216
+ else if (options && typeof options === 'object') {
217
+ if (typeof options.colors === 'boolean') {
218
+ useColors = options.colors;
219
+ }
220
+ else if (typeof options.color === 'boolean') {
221
+ useColors = options.color;
222
+ }
223
+ else if (options.colors === 'true' || options.color === 'true') {
224
+ useColors = true;
225
+ }
226
+ else if (options.colors === 'false' || options.color === 'false' || options['no-color']) {
227
+ useColors = false;
228
+ }
229
+ }
230
+ const c = createColors(useColors);
231
+ // Sanitize untrusted log values first to prevent terminal injection
232
+ const timestamp = (0, logger_1.sanitizeString)(event.timestamp || 'UNKNOWN-TIME', 30);
233
+ const severity = (0, logger_1.sanitizeString)((event.severity || 'INFO'), 10).toUpperCase();
234
+ const type = (0, logger_1.sanitizeString)(event.type || 'unknown', 20);
235
+ const rawAction = String(event.action || 'unknown').toUpperCase();
236
+ const normalizedAction = rawAction === 'WARN' ? 'WARNED'
237
+ : rawAction === 'BLOCK' ? 'BLOCKED'
238
+ : rawAction === 'ALLOW' ? 'ALLOWED'
239
+ : rawAction;
240
+ const action = (0, logger_1.sanitizeString)(normalizedAction, 10);
241
+ const ip = (0, logger_1.sanitizeString)(event.ip || 'unknown', 45);
242
+ const method = (0, logger_1.sanitizeString)(event.method || 'GET', 10).toUpperCase();
243
+ const route = (0, logger_1.sanitizeString)(event.route || '/', 100);
244
+ const reason = (0, logger_1.sanitizeString)(event.reason || 'No details', 150);
245
+ if (!useColors) {
246
+ return `[${timestamp}] [${severity}] [${action}] ${method} ${route} | IP: ${ip} | ${type}: ${reason}`;
247
+ }
248
+ // Action badge with semantic color
249
+ let styledAction;
250
+ switch (action) {
251
+ case 'BLOCKED':
252
+ styledAction = c.bold(c.red(`[${action}]`));
253
+ break;
254
+ case 'WARNED':
255
+ styledAction = c.bold(c.yellow(`[${action}]`));
256
+ break;
257
+ case 'ALLOWED':
258
+ styledAction = c.bold(c.green(`[${action}]`));
259
+ break;
260
+ case 'UNBLOCKED':
261
+ styledAction = c.bold(c.cyan(`[${action}]`));
262
+ break;
263
+ case 'REQUESTED':
264
+ styledAction = c.bold(c.blue(`[${action}]`));
265
+ break;
266
+ default:
267
+ styledAction = c.bold(`[${action}]`);
268
+ break;
269
+ }
270
+ // Severity badge with semantic color
271
+ let styledSeverity;
272
+ switch (severity) {
273
+ case 'CRITICAL':
274
+ styledSeverity = c.bold(c.red(`[${severity}]`));
275
+ break;
276
+ case 'WARNING':
277
+ styledSeverity = c.bold(c.yellow(`[${severity}]`));
278
+ break;
279
+ case 'INFO':
280
+ styledSeverity = c.bold(c.green(`[${severity}]`));
281
+ break;
282
+ default:
283
+ styledSeverity = c.bold(`[${severity}]`);
284
+ break;
285
+ }
286
+ const styledTimestamp = c.gray(`[${timestamp}]`);
287
+ const styledMethod = c.bold(method);
288
+ const styledRoute = c.cyan(route);
289
+ const styledIp = c.bold(ip);
290
+ const styledType = c.yellow(type);
291
+ return `${styledTimestamp} ${styledSeverity} ${styledAction} ${styledMethod} ${styledRoute} | IP: ${styledIp} | ${styledType}: ${reason}`;
292
+ }
293
+ /**
294
+ * Reads and parses the last N records from the JSON Lines file.
295
+ * Handles missing files and skips malformed records gracefully.
296
+ *
297
+ * @param logFilePath - Path to log file
298
+ * @param limit - Max number of events to return
299
+ * @returns Extracted events and counts
300
+ */
301
+ function readLogs(logFilePath, limit = 20) {
302
+ if (!node_fs_1.default.existsSync(logFilePath)) {
303
+ return { events: [], totalLines: 0, malformedCount: 0, fileExists: false };
304
+ }
305
+ const rawContent = node_fs_1.default.readFileSync(logFilePath, 'utf8');
306
+ const rawLines = rawContent.split('\n').filter(line => line.trim().length > 0);
307
+ let malformedCount = 0;
308
+ const parsedEvents = [];
309
+ for (const line of rawLines) {
310
+ try {
311
+ const parsed = JSON.parse(line);
312
+ parsedEvents.push(parsed);
313
+ }
314
+ catch {
315
+ malformedCount++;
316
+ }
317
+ }
318
+ const selected = parsedEvents.slice(-limit);
319
+ return {
320
+ events: selected,
321
+ totalLines: rawLines.length,
322
+ malformedCount,
323
+ fileExists: true
324
+ };
325
+ }
326
+ /**
327
+ * Sanitizes untrusted IP address values from logs for safe status aggregation and output.
328
+ * Strips ANSI escapes, truncates at newlines/control characters, and extracts valid IP addresses.
329
+ *
330
+ * @param rawIp - Untrusted IP candidate from logs
331
+ * @returns Normalized IP address or 'unknown'
332
+ */
333
+ function sanitizeIp(rawIp) {
334
+ if (!rawIp || typeof rawIp !== 'string')
335
+ return 'unknown';
336
+ const trimmed = rawIp.trim();
337
+ if (trimmed.length === 0)
338
+ return 'unknown';
339
+ if (node_net_1.default.isIP(trimmed) !== 0) {
340
+ return (0, block_manager_1.normalizeIp)(trimmed);
341
+ }
342
+ const stripped = (0, logger_1.stripAnsi)(trimmed);
343
+ if (node_net_1.default.isIP(stripped) !== 0) {
344
+ return (0, block_manager_1.normalizeIp)(stripped);
345
+ }
346
+ const beforeEscape = (trimmed.split(/[\x1b\r\n\x00-\x1f\x7f]/)[0] ?? '').trim();
347
+ if (beforeEscape.length > 0 && node_net_1.default.isIP(beforeEscape) !== 0) {
348
+ return (0, block_manager_1.normalizeIp)(beforeEscape);
349
+ }
350
+ 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]?)/);
351
+ if (ipv4Match && ipv4Match[0] && node_net_1.default.isIP(ipv4Match[0]) === 4) {
352
+ return (0, block_manager_1.normalizeIp)(ipv4Match[0]);
353
+ }
354
+ const ipv6Candidates = stripped.match(/[0-9a-fA-F:]+/g);
355
+ if (ipv6Candidates) {
356
+ for (const cand of ipv6Candidates) {
357
+ if (node_net_1.default.isIP(cand) === 6) {
358
+ return (0, block_manager_1.normalizeIp)(cand);
359
+ }
360
+ }
361
+ }
362
+ return 'unknown';
363
+ }
364
+ /**
365
+ * Sanitizes untrusted event type strings from logs.
366
+ *
367
+ * @param rawType - Untrusted event type string
368
+ * @param maxLength - Maximum permitted length
369
+ * @returns Clean event type string
370
+ */
371
+ function sanitizeType(rawType, maxLength = 30) {
372
+ if (!rawType || typeof rawType !== 'string')
373
+ return 'unknown';
374
+ let clean = rawType.split(/[\r\n\x1b]/)[0] ?? '';
375
+ clean = (0, logger_1.stripAnsi)(clean);
376
+ clean = clean.replace(/[\x00-\x1f\x7f]/g, ' ').trim();
377
+ clean = clean.replace(/\s+/g, ' ');
378
+ return clean.slice(0, maxLength) || 'unknown';
379
+ }
380
+ /**
381
+ * Sanitizes untrusted action strings from logs.
382
+ *
383
+ * @param rawAction - Untrusted action string
384
+ * @param maxLength - Maximum permitted length
385
+ * @returns Clean action string
386
+ */
387
+ function sanitizeAction(rawAction, maxLength = 20) {
388
+ if (!rawAction || typeof rawAction !== 'string')
389
+ return 'unknown';
390
+ let clean = rawAction.split(/[\r\n\x1b]/)[0] ?? '';
391
+ clean = (0, logger_1.stripAnsi)(clean);
392
+ clean = clean.replace(/[\x00-\x1f\x7f]/g, '').trim().toLowerCase();
393
+ if (clean === 'warn')
394
+ clean = 'warned';
395
+ else if (clean === 'block')
396
+ clean = 'blocked';
397
+ else if (clean === 'allow')
398
+ clean = 'allowed';
399
+ return clean.slice(0, maxLength) || 'unknown';
400
+ }
401
+ /**
402
+ * Computes security summary statistics from the log file.
403
+ *
404
+ * @param logFilePath - Path to log file
405
+ * @returns Summary metrics
406
+ */
407
+ function calculateStatus(logFilePath) {
408
+ if (!node_fs_1.default.existsSync(logFilePath)) {
409
+ return { fileExists: false, totalEvents: 0 };
410
+ }
411
+ const { events: allEvents, malformedCount } = readLogs(logFilePath, 100000);
412
+ const ipCounts = Object.create(null);
413
+ const routeCounts = Object.create(null);
414
+ const typeCounts = Object.create(null);
415
+ const actionCounts = Object.create(null);
416
+ for (const event of allEvents) {
417
+ const ip = sanitizeIp(event.ip);
418
+ const route = (0, logger_1.sanitizeRoute)(event.route);
419
+ const type = sanitizeType(event.type);
420
+ const action = sanitizeAction(event.action);
421
+ ipCounts[ip] = (ipCounts[ip] || 0) + 1;
422
+ routeCounts[route] = (routeCounts[route] || 0) + 1;
423
+ typeCounts[type] = (typeCounts[type] || 0) + 1;
424
+ actionCounts[action] = (actionCounts[action] || 0) + 1;
425
+ }
426
+ // Sort top offenders
427
+ const topIps = Object.entries(ipCounts)
428
+ .sort((a, b) => b[1] - a[1])
429
+ .slice(0, 5)
430
+ .map(([ip, count]) => ({ ip, count }));
431
+ const topRoutes = Object.entries(routeCounts)
432
+ .sort((a, b) => b[1] - a[1])
433
+ .slice(0, 5)
434
+ .map(([route, count]) => ({ route, count }));
435
+ return {
436
+ fileExists: true,
437
+ totalEvents: allEvents.length,
438
+ malformedCount,
439
+ typeCounts,
440
+ actionCounts,
441
+ topIps,
442
+ topRoutes
443
+ };
444
+ }
445
+ /**
446
+ * Main command runner. Returns an exit code (0 for success, 1 for error).
447
+ *
448
+ * @param argv - Arguments array
449
+ * @param output - Output destination (console or mock object)
450
+ * @returns Exit code
451
+ */
452
+ async function runCommand(argv = process.argv.slice(2), output = console) {
453
+ const { command, args, options } = parseCliArgs(argv);
454
+ const logFilePath = resolveLogPath(options);
455
+ const useColors = resolveColorSupport(options, output);
456
+ const c = createColors(useColors);
457
+ switch (command) {
458
+ case 'status': {
459
+ output.log(c.cyan('\n\n===================================================='));
460
+ output.log(c.bold(c.cyan('🛡️ Z-Shield Security Status')));
461
+ output.log(c.cyan('===================================================='));
462
+ output.log(`${c.bold('Log File:')} ${c.gray(logFilePath)}`);
463
+ const status = calculateStatus(logFilePath);
464
+ if (!status.fileExists) {
465
+ output.log(`${c.bold('Status:')} ${c.yellow('No log file found yet.')}`);
466
+ output.log(`\n${c.bold('Checked Location:')} ${c.gray(logFilePath)}`);
467
+ output.log(c.dim('ℹ Start your Express or Next.js server to begin logging security events.'));
468
+ output.log(c.dim(' If your application stores logs in a custom directory, specify it with:'));
469
+ output.log(c.dim(' • CLI flag: z-shield status --log-dir <path>'));
470
+ output.log(c.dim(' • Environment variable: Z_SHIELD_LOG_DIR=<path> z-shield status'));
471
+ output.log(c.cyan('====================================================\n'));
472
+ return 0;
473
+ }
474
+ output.log(`${c.bold('Total Events Recorded:')} ${c.bold(c.cyan(status.totalEvents))}`);
475
+ if (status.malformedCount && status.malformedCount > 0) {
476
+ output.log(`${c.bold(c.yellow('⚠️ Malformed Lines Skipped:'))} ${c.yellow(status.malformedCount)}`);
477
+ }
478
+ output.log(`\n${c.bold('Actions Breakdown:')}`);
479
+ if (status.actionCounts) {
480
+ for (const [action, count] of Object.entries(status.actionCounts)) {
481
+ const cleanAction = sanitizeAction(action);
482
+ const upper = cleanAction.toUpperCase();
483
+ let styledAction = upper;
484
+ if (upper === 'BLOCKED')
485
+ styledAction = c.bold(c.red(upper));
486
+ else if (upper === 'WARNED')
487
+ styledAction = c.bold(c.yellow(upper));
488
+ else if (upper === 'ALLOWED')
489
+ styledAction = c.bold(c.green(upper));
490
+ else if (upper === 'UNBLOCKED')
491
+ styledAction = c.bold(c.cyan(upper));
492
+ else if (upper === 'REQUESTED')
493
+ styledAction = c.bold(c.blue(upper));
494
+ output.log(` - ${styledAction}: ${c.bold(count)}`);
495
+ }
496
+ }
497
+ output.log(`\n${c.bold('Event Types:')}`);
498
+ if (status.typeCounts) {
499
+ for (const [type, count] of Object.entries(status.typeCounts)) {
500
+ const cleanType = sanitizeType(type);
501
+ output.log(` - ${c.yellow(cleanType)}: ${c.bold(count)}`);
502
+ }
503
+ }
504
+ if (status.topIps && status.topIps.length > 0) {
505
+ output.log(`\n${c.bold('Top Offending IPs:')}`);
506
+ for (const item of status.topIps) {
507
+ const cleanIp = sanitizeIp(item.ip);
508
+ output.log(` - ${c.cyan(cleanIp)}: ${c.bold(item.count)} events`);
509
+ }
510
+ }
511
+ if (status.topRoutes && status.topRoutes.length > 0) {
512
+ output.log(`\n${c.bold('Top Targeted Routes:')}`);
513
+ for (const item of status.topRoutes) {
514
+ const cleanRoute = (0, logger_1.sanitizeRoute)(item.route);
515
+ output.log(` - ${c.underline(cleanRoute)}: ${c.bold(item.count)} events`);
516
+ }
517
+ }
518
+ output.log(`\n${c.dim('ℹ Process-Local Architecture Notice:')}`);
519
+ output.log(c.dim('Statistics reflect historical events recorded in the local log file.'));
520
+ output.log(c.dim('Active in-memory counters and temporary blocks reside in server RAM'));
521
+ output.log(c.dim('and are managed inside the running Express process.'));
522
+ output.log(c.cyan('===================================================='));
523
+ return 0;
524
+ }
525
+ case 'logs': {
526
+ let limit = 20;
527
+ if (options.limit) {
528
+ const parsed = parseInt(String(options.limit), 10);
529
+ if (isNaN(parsed) || parsed <= 0) {
530
+ output.error(c.bold(c.red('[ERROR] --limit must be a positive integer.')));
531
+ return 1;
532
+ }
533
+ limit = parsed;
534
+ }
535
+ const { events, totalLines, malformedCount, fileExists } = readLogs(logFilePath, limit);
536
+ output.log(c.cyan('\n\n===================================================='));
537
+ output.log(c.bold(c.cyan(`🛡️ Z-Shield Security Logs (Showing last ${events.length} of ${totalLines} events)`)));
538
+ output.log(`${c.bold('Source:')} ${c.gray(logFilePath)}`);
539
+ output.log(c.cyan('===================================================='));
540
+ if (!fileExists || events.length === 0) {
541
+ if (!fileExists) {
542
+ output.log(c.yellow(`ℹ No log file found at: ${logFilePath}`));
543
+ output.log(c.dim(' Ensure your Express or Next.js server has started and recorded events.'));
544
+ output.log(c.dim(' If your logs are stored in a custom directory, specify it with:'));
545
+ output.log(c.dim(' • CLI flag: z-shield logs --log-dir <path>'));
546
+ output.log(c.dim(' • Environment variable: Z_SHIELD_LOG_DIR=<path> z-shield logs'));
547
+ }
548
+ else {
549
+ output.log(c.dim('ℹ No security events found in log file.'));
550
+ }
551
+ output.log(c.cyan('===================================================='));
552
+ return 0;
553
+ }
554
+ for (const event of events) {
555
+ output.log(formatLogEvent(event, { colors: useColors }));
556
+ }
557
+ if (malformedCount > 0) {
558
+ output.log(`\n${c.yellow(`⚠️ Notice: Skipped ${malformedCount} unparseable or corrupted lines.`)}`);
559
+ }
560
+ output.log(c.cyan('===================================================='));
561
+ return 0;
562
+ }
563
+ case 'monitor': {
564
+ output.log(c.cyan('\n\n===================================================='));
565
+ output.log(c.bold(c.cyan('🛡️ Z-Shield Live Security Event Monitor')));
566
+ output.log(`${c.bold('Watching:')} ${c.gray(logFilePath)}`);
567
+ output.log(c.dim('Press Ctrl+C to exit.'));
568
+ output.log(c.cyan('===================================================='));
569
+ let currentSize = 0;
570
+ if (node_fs_1.default.existsSync(logFilePath)) {
571
+ currentSize = node_fs_1.default.statSync(logFilePath).size;
572
+ // Show recent events for immediate context
573
+ const { events } = readLogs(logFilePath, 5);
574
+ if (events.length > 0) {
575
+ output.log(c.dim('--- Recent Events ---'));
576
+ for (const ev of events) {
577
+ output.log(formatLogEvent(ev, { colors: useColors }));
578
+ }
579
+ output.log(c.dim('--- Live Stream Active ---'));
580
+ }
581
+ }
582
+ else {
583
+ output.log(c.yellow(`ℹ Waiting for log file to be created at: ${logFilePath}`));
584
+ output.log(c.dim(' Tip: You can specify a custom directory with --log-dir <path> or Z_SHIELD_LOG_DIR=<path>.'));
585
+ }
586
+ // Live watcher polling function state
587
+ let isReading = false;
588
+ let partialLine = '';
589
+ const checkNewLines = () => {
590
+ if (!node_fs_1.default.existsSync(logFilePath) || isReading) {
591
+ return;
592
+ }
593
+ try {
594
+ const stats = node_fs_1.default.statSync(logFilePath);
595
+ if (stats.size > currentSize) {
596
+ isReading = true;
597
+ const readStart = currentSize;
598
+ const readEnd = stats.size;
599
+ currentSize = readEnd;
600
+ const stream = node_fs_1.default.createReadStream(logFilePath, {
601
+ start: readStart,
602
+ end: readEnd,
603
+ encoding: 'utf8'
604
+ });
605
+ stream.on('data', chunk => {
606
+ partialLine += chunk;
607
+ const lines = partialLine.split('\n');
608
+ partialLine = lines.pop() ?? ''; // keep last partial line
609
+ for (const line of lines) {
610
+ if (line.trim().length > 0) {
611
+ try {
612
+ const parsed = JSON.parse(line);
613
+ output.log(formatLogEvent(parsed, { colors: useColors }));
614
+ }
615
+ catch {
616
+ output.log(c.yellow('[⚠️ Corrupted log record]'));
617
+ }
618
+ }
619
+ }
620
+ });
621
+ stream.on('end', () => {
622
+ isReading = false;
623
+ });
624
+ stream.on('error', () => {
625
+ isReading = false;
626
+ });
627
+ }
628
+ else if (stats.size < currentSize) {
629
+ // File was rotated or truncated!
630
+ currentSize = 0;
631
+ partialLine = '';
632
+ }
633
+ }
634
+ catch {
635
+ isReading = false;
636
+ }
637
+ };
638
+ // Poll every 300ms for responsive terminal updates
639
+ const interval = setInterval(checkNewLines, 300);
640
+ // Clean exit on SIGINT (Ctrl+C)
641
+ return new Promise((resolve) => {
642
+ const cleanup = () => {
643
+ clearInterval(interval);
644
+ output.log(`\n${c.cyan('👋 Exiting Z-Shield live monitor.')}`);
645
+ resolve(0);
646
+ };
647
+ process.once('SIGINT', cleanup);
648
+ process.once('SIGTERM', cleanup);
649
+ });
650
+ }
651
+ case 'block': {
652
+ const ip = args[0];
653
+ if (!ip) {
654
+ output.error(c.bold(c.red('[ERROR] Missing IP address. Usage: npx z-shield block <ip> [--duration 10m] [--reason <text>]')));
655
+ return 1;
656
+ }
657
+ if (!(0, block_manager_1.isValidIp)(ip)) {
658
+ output.error(c.bold(c.red(`[ERROR] Invalid IP address "${ip}". Must be a valid IPv4 or IPv6 address.`)));
659
+ return 1;
660
+ }
661
+ const durationRaw = options.duration || '15m';
662
+ let durationMs;
663
+ try {
664
+ durationMs = (0, block_manager_1.parseDuration)(durationRaw);
665
+ }
666
+ catch (err) {
667
+ output.error(c.bold(c.red(`[ERROR] ${err.message}`)));
668
+ return 1;
669
+ }
670
+ const reason = options.reason || 'Manual block requested via CLI';
671
+ output.log(c.cyan('\n\n===================================================='));
672
+ output.log(c.bold(c.cyan('🛡️ Z-Shield CLI: Block Request')));
673
+ output.log(c.cyan('===================================================='));
674
+ output.log(`${c.bold('Target IP:')} ${c.bold(c.cyan(ip))}`);
675
+ output.log(`${c.bold('Duration:')} ${c.yellow(durationRaw)} (${durationMs.toLocaleString()}ms)`);
676
+ output.log(`${c.bold('Reason:')} ${reason}`);
677
+ output.log(`\n${c.bold(c.yellow('ℹ Process-Local Architecture Notice:'))}`);
678
+ output.log(c.dim('Z-Shield manages active temporary blocks in server memory (RAM).'));
679
+ output.log(c.dim('Because this CLI runs in an independent Node.js process, it cannot'));
680
+ output.log(c.dim("directly modify the running Express server's memory heap without an"));
681
+ output.log(c.dim('external IPC socket or database (which Z-Shield intentionally avoids).'));
682
+ output.log(`\n${c.bold('To block this IP in your running Express application:')}`);
683
+ output.log(c.green(` shield.block('${ip}', '${durationRaw}', '${reason}');`));
684
+ // Record an audit log event
685
+ try {
686
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(logFilePath), { recursive: true });
687
+ const auditEvent = {
688
+ timestamp: new Date().toISOString(),
689
+ appName: 'cli',
690
+ type: 'cli_block_requested',
691
+ severity: 'info',
692
+ ip,
693
+ method: 'CLI',
694
+ route: '/cli/block',
695
+ reason: `Requested duration: ${durationRaw} | Reason: ${reason}`,
696
+ action: 'requested'
697
+ };
698
+ node_fs_1.default.appendFileSync(logFilePath, JSON.stringify(auditEvent) + '\n');
699
+ output.log(`\n${c.green('✔')} Audit entry recorded in: ${c.gray(logFilePath)}`);
700
+ }
701
+ catch {
702
+ // Safe fallback
703
+ }
704
+ output.log(c.cyan('===================================================='));
705
+ return 0;
706
+ }
707
+ case 'unblock': {
708
+ const ip = args[0];
709
+ if (!ip) {
710
+ output.error(c.bold(c.red('[ERROR] Missing IP address. Usage: npx z-shield unblock <ip>')));
711
+ return 1;
712
+ }
713
+ if (!(0, block_manager_1.isValidIp)(ip)) {
714
+ output.error(c.bold(c.red(`[ERROR] Invalid IP address "${ip}". Must be a valid IPv4 or IPv6 address.`)));
715
+ return 1;
716
+ }
717
+ output.log(c.cyan('\n\n===================================================='));
718
+ output.log(c.bold(c.cyan('🛡️ Z-Shield CLI: Unblock Request')));
719
+ output.log(c.cyan('===================================================='));
720
+ output.log(`${c.bold('Target IP:')} ${c.bold(c.cyan(ip))}`);
721
+ output.log(`\n${c.bold(c.yellow('ℹ Process-Local Architecture Notice:'))}`);
722
+ output.log(c.dim('Z-Shield manages active temporary blocks in server memory (RAM).'));
723
+ output.log(`\n${c.bold('To unblock this IP in your running Express application:')}`);
724
+ output.log(c.green(` shield.unblock('${ip}');`));
725
+ // Record an audit log event
726
+ try {
727
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(logFilePath), { recursive: true });
728
+ const auditEvent = {
729
+ timestamp: new Date().toISOString(),
730
+ appName: 'cli',
731
+ type: 'cli_unblock_requested',
732
+ severity: 'info',
733
+ ip,
734
+ method: 'CLI',
735
+ route: '/cli/unblock',
736
+ reason: 'Manual unblock requested via CLI',
737
+ action: 'requested'
738
+ };
739
+ node_fs_1.default.appendFileSync(logFilePath, JSON.stringify(auditEvent) + '\n');
740
+ output.log(`\n${c.green('✔')} Audit entry recorded in: ${c.gray(logFilePath)}`);
741
+ }
742
+ catch {
743
+ // Safe fallback
744
+ }
745
+ output.log(c.cyan('===================================================='));
746
+ return 0;
747
+ }
748
+ case 'help':
749
+ default: {
750
+ output.log(c.cyan('\n\n===================================================='));
751
+ output.log(c.bold(c.cyan('🛡️ Z-Shield CLI — API Abuse Detection & Protection')));
752
+ output.log(c.cyan('===================================================='));
753
+ output.log(c.bold('Usage:'));
754
+ output.log(` ${c.bold('npx z-shield')} ${c.cyan('<command>')} [options]`);
755
+ output.log(`\n${c.bold('Commands:')}`);
756
+ output.log(` ${c.bold('npx z-shield status')} Show security overview and top offender IPs`);
757
+ output.log(` ${c.bold('npx z-shield logs')} [--limit 50] Display recent security event logs`);
758
+ output.log(` ${c.bold('npx z-shield monitor')} Stream security events in real time`);
759
+ output.log(` ${c.bold('npx z-shield block')} <ip> Instructions & audit for blocking an IP`);
760
+ output.log(` ${c.bold('npx z-shield unblock')} <ip> Instructions & audit for unblocking an IP`);
761
+ output.log(`\n${c.bold('Options:')}`);
762
+ output.log(` ${c.yellow('--limit')} <number> Number of log entries to display (default: 20)`);
763
+ output.log(` ${c.yellow('--duration')} <time> Block duration (e.g. 10m, 1h, 30s)`);
764
+ output.log(` ${c.yellow('--reason')} <text> Reason for manual block`);
765
+ output.log(` ${c.yellow('--log-dir')} <path> Path to log directory (default: .z-shield-logs)`);
766
+ output.log(` ${c.yellow('--color')} Force enable colored terminal output`);
767
+ output.log(` ${c.yellow('--no-color')} Disable colored terminal output`);
768
+ output.log(` ${c.yellow('--help, -h')} Display this help message`);
769
+ output.log(`\n${c.bold('Environment Variables:')}`);
770
+ output.log(` ${c.yellow('Z_SHIELD_LOG_DIR')} Default log directory path when --log-dir is omitted`);
771
+ output.log(c.cyan('===================================================='));
772
+ return command === 'help' ? 0 : 1;
773
+ }
774
+ }
775
+ }
776
+ // Execute directly if run from CLI
777
+ if (require.main === module) {
778
+ runCommand(process.argv.slice(2)).then(code => {
779
+ if (code !== 0) {
780
+ process.exit(code);
781
+ }
782
+ });
783
+ }
784
+ //# sourceMappingURL=cli.js.map