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.
- package/README.md +123 -6
- package/dist/block-manager.d.ts +134 -0
- package/dist/block-manager.d.ts.map +1 -0
- package/dist/block-manager.js +425 -0
- package/dist/block-manager.js.map +1 -0
- package/dist/cli.d.ts +141 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +784 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/config.js.map +1 -0
- package/dist/detector.d.ts +56 -0
- package/dist/detector.d.ts.map +1 -0
- package/dist/detector.js +81 -0
- package/dist/detector.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +141 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware.d.ts +68 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +272 -0
- package/dist/middleware.js.map +1 -0
- package/dist/next.d.ts +73 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +486 -0
- package/dist/next.js.map +1 -0
- package/dist/rate-limiter.d.ts +75 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +153 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/next.d.ts +1 -0
- package/next.js +2 -0
- package/package.json +34 -5
- package/src/block-manager.js +0 -326
- package/src/cli.js +0 -680
- package/src/config.js +0 -165
- package/src/detector.js +0 -88
- package/src/index.js +0 -97
- package/src/logger.js +0 -279
- package/src/middleware.js +0 -288
- 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
|