z-shield 0.1.1 → 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 -764
  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 -280
  51. package/src/middleware.js +0 -288
  52. package/src/rate-limiter.js +0 -186
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.extractNextClientIp = exports.withNextShield = exports.createNextShield = exports.sanitizeMethod = exports.sanitizeRoute = exports.sanitizeString = exports.stripAnsi = exports.Logger = exports.parseDuration = exports.canonicalizeIpv6 = exports.normalizeIp = exports.isValidIp = exports.BlockManager = exports.Detector = exports.RateLimiter = exports.DEFAULT_CONFIG = exports.resolveConfig = exports.safelyEndResponse = exports.isRouteExcluded = exports.extractClientIp = exports.createMiddleware = void 0;
18
+ exports.createShield = createShield;
19
+ const middleware_1 = require("./middleware");
20
+ Object.defineProperty(exports, "createMiddleware", { enumerable: true, get: function () { return middleware_1.createMiddleware; } });
21
+ Object.defineProperty(exports, "extractClientIp", { enumerable: true, get: function () { return middleware_1.extractClientIp; } });
22
+ Object.defineProperty(exports, "isRouteExcluded", { enumerable: true, get: function () { return middleware_1.isRouteExcluded; } });
23
+ Object.defineProperty(exports, "safelyEndResponse", { enumerable: true, get: function () { return middleware_1.safelyEndResponse; } });
24
+ const next_1 = require("./next");
25
+ Object.defineProperty(exports, "createNextShield", { enumerable: true, get: function () { return next_1.createNextShield; } });
26
+ Object.defineProperty(exports, "withNextShield", { enumerable: true, get: function () { return next_1.withNextShield; } });
27
+ Object.defineProperty(exports, "extractNextClientIp", { enumerable: true, get: function () { return next_1.extractNextClientIp; } });
28
+ const config_1 = require("./config");
29
+ Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_1.resolveConfig; } });
30
+ Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return config_1.DEFAULT_CONFIG; } });
31
+ const rate_limiter_1 = require("./rate-limiter");
32
+ Object.defineProperty(exports, "RateLimiter", { enumerable: true, get: function () { return rate_limiter_1.RateLimiter; } });
33
+ const detector_1 = require("./detector");
34
+ Object.defineProperty(exports, "Detector", { enumerable: true, get: function () { return detector_1.Detector; } });
35
+ const block_manager_1 = require("./block-manager");
36
+ Object.defineProperty(exports, "BlockManager", { enumerable: true, get: function () { return block_manager_1.BlockManager; } });
37
+ Object.defineProperty(exports, "isValidIp", { enumerable: true, get: function () { return block_manager_1.isValidIp; } });
38
+ Object.defineProperty(exports, "normalizeIp", { enumerable: true, get: function () { return block_manager_1.normalizeIp; } });
39
+ Object.defineProperty(exports, "canonicalizeIpv6", { enumerable: true, get: function () { return block_manager_1.canonicalizeIpv6; } });
40
+ Object.defineProperty(exports, "parseDuration", { enumerable: true, get: function () { return block_manager_1.parseDuration; } });
41
+ const logger_1 = require("./logger");
42
+ Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_1.Logger; } });
43
+ Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return logger_1.stripAnsi; } });
44
+ Object.defineProperty(exports, "sanitizeString", { enumerable: true, get: function () { return logger_1.sanitizeString; } });
45
+ Object.defineProperty(exports, "sanitizeRoute", { enumerable: true, get: function () { return logger_1.sanitizeRoute; } });
46
+ Object.defineProperty(exports, "sanitizeMethod", { enumerable: true, get: function () { return logger_1.sanitizeMethod; } });
47
+ /**
48
+ * Main factory function for initializing Z-Shield.
49
+ *
50
+ * Usage in Express:
51
+ * const { createShield } = require('z-shield');
52
+ * const shield = createShield({ mode: 'protect', maxRequests: 50 });
53
+ * app.use(shield.middleware);
54
+ *
55
+ * Programmatic Block API:
56
+ * shield.block('198.51.100.1', '10m', 'Manual administrative block');
57
+ * shield.unblock('198.51.100.1');
58
+ * shield.isBlocked('198.51.100.1');
59
+ *
60
+ * @param options - Custom configuration options
61
+ * @returns Z-Shield instance with middleware, managers, logger, and programmatic methods
62
+ */
63
+ function createShield(options = {}) {
64
+ const config = (0, config_1.resolveConfig)(options);
65
+ const rateLimiter = new rate_limiter_1.RateLimiter(config);
66
+ const detector = new detector_1.Detector({ rateLimiter, config });
67
+ const blockManager = new block_manager_1.BlockManager(config);
68
+ const logger = new logger_1.Logger(config);
69
+ const middleware = (0, middleware_1.createMiddleware)(config, {
70
+ rateLimiter,
71
+ detector,
72
+ blockManager,
73
+ logger
74
+ });
75
+ return {
76
+ middleware,
77
+ config,
78
+ rateLimiter,
79
+ detector,
80
+ blockManager,
81
+ logger,
82
+ // Programmatic Block API with security event logging
83
+ block: (ip, duration, reason) => {
84
+ const result = blockManager.block(ip, duration, reason);
85
+ void logger.log({
86
+ appName: config.appName,
87
+ type: 'manual_block',
88
+ severity: 'info',
89
+ ip,
90
+ method: 'INTERNAL',
91
+ route: '/admin/block',
92
+ reason: result.reason,
93
+ action: 'blocked'
94
+ });
95
+ return result;
96
+ },
97
+ unblock: (ip) => {
98
+ const result = blockManager.unblock(ip);
99
+ if (result) {
100
+ void logger.log({
101
+ appName: config.appName,
102
+ type: 'manual_unblock',
103
+ severity: 'info',
104
+ ip,
105
+ method: 'INTERNAL',
106
+ route: '/admin/unblock',
107
+ reason: 'Manual unblock requested',
108
+ action: 'unblocked'
109
+ });
110
+ }
111
+ return result;
112
+ },
113
+ isBlocked: (ip) => blockManager.isBlocked(ip),
114
+ getBlockInfo: (ip) => blockManager.getBlockInfo(ip),
115
+ listBlocks: () => blockManager.listBlocks(),
116
+ flush: () => logger.flush()
117
+ };
118
+ }
119
+ __exportStar(require("./types"), exports);
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AAAA,6CAAqG;iGAA5F,6BAAgB;gGAAE,4BAAe;gGAAE,4BAAe;kGAAE,8BAAiB;AAC9E,iCAA+E;iGAAtE,uBAAgB;+FAAE,qBAAc;oGAAE,0BAAmB;AAC9D,qCAAyD;8FAAhD,sBAAa;+FAAE,uBAAc;AACtC,iDAA6C;4FAApC,0BAAW;AACpB,yCAAsC;yFAA7B,mBAAQ;AACjB,mDAAwH;6FAA/G,4BAAY;0FAAE,yBAAS;4FAAE,2BAAW;iGAAE,gCAAgB;8FAAE,6BAAa;AAC9E,qCAA4F;uFAAnF,eAAM;0FAAE,kBAAS;+FAAE,uBAAc;8FAAE,sBAAa;+FAAE,uBAAc;AASzE;;;;;;;;;;;;;;;GAeG;AACH,sBAA6B,OAAO,GAAmC,EAAE;IACvE,MAAM,MAAM,GAAG,IAAA,sBAAa,EAAC,OAAO,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,0BAAW,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,IAAI,mBAAQ,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,MAAM,YAAY,GAAG,IAAI,4BAAY,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,eAAM,CAAC,MAAM,CAAC,CAAC;IAElC,MAAM,UAAU,GAAG,IAAA,6BAAgB,EAAC,MAAM,EAAE;QAC1C,WAAW;QACX,QAAQ;QACR,YAAY;QACZ,MAAM;KACP,CAAC,CAAC;IAEH,OAAO;QACL,UAAU;QACV,MAAM;QACN,WAAW;QACX,QAAQ;QACR,YAAY;QACZ,MAAM;QAEN,qDAAqD;QACrD,KAAK,EAAE,CAAC,EAAU,EAAE,QAA0B,EAAE,MAAe,EAAe,EAAE;YAC9E,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACxD,KAAK,MAAM,CAAC,GAAG,CAAC;gBACd,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EAAE,cAAc;gBACpB,QAAQ,EAAE,MAAM;gBAChB,EAAE;gBACF,MAAM,EAAE,UAAU;gBAClB,KAAK,EAAE,cAAc;gBACrB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,MAAM,EAAE,SAAS;aAClB,CAAC,CAAC;YACH,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,OAAO,EAAE,CAAC,EAAU,EAAW,EAAE;YAC/B,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACxC,IAAI,MAAM,EAAE,CAAC;gBACX,KAAK,MAAM,CAAC,GAAG,CAAC;oBACd,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,IAAI,EAAE,gBAAgB;oBACtB,QAAQ,EAAE,MAAM;oBAChB,EAAE;oBACF,MAAM,EAAE,UAAU;oBAClB,KAAK,EAAE,gBAAgB;oBACvB,MAAM,EAAE,0BAA0B;oBAClC,MAAM,EAAE,WAAW;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,SAAS,EAAE,CAAC,EAAU,EAAW,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9D,YAAY,EAAE,CAAC,EAAU,EAAoB,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7E,UAAU,EAAE,GAAgB,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE;QACxD,KAAK,EAAE,GAAkB,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC3C,CAAC;AACJ,CAAC;AA2BD,0CAAwB","sourcesContent":["import { createMiddleware, extractClientIp, isRouteExcluded, safelyEndResponse } from './middleware';\nimport { createNextShield, withNextShield, extractNextClientIp } from './next';\nimport { resolveConfig, DEFAULT_CONFIG } from './config';\nimport { RateLimiter } from './rate-limiter';\nimport { Detector } from './detector';\nimport { BlockManager, isValidIp, normalizeIp, canonicalizeIpv6, parseDuration, type BlockInfo } from './block-manager';\nimport { Logger, stripAnsi, sanitizeString, sanitizeRoute, sanitizeMethod } from './logger';\nimport type {\n ZShieldConfig,\n ZShieldOptions,\n BlockRecord,\n ZShieldInstance,\n ZShieldFullInstance\n} from './types';\n\n/**\n * Main factory function for initializing Z-Shield.\n *\n * Usage in Express:\n * const { createShield } = require('z-shield');\n * const shield = createShield({ mode: 'protect', maxRequests: 50 });\n * app.use(shield.middleware);\n *\n * Programmatic Block API:\n * shield.block('198.51.100.1', '10m', 'Manual administrative block');\n * shield.unblock('198.51.100.1');\n * shield.isBlocked('198.51.100.1');\n *\n * @param options - Custom configuration options\n * @returns Z-Shield instance with middleware, managers, logger, and programmatic methods\n */\nexport function createShield(options: ZShieldOptions | ZShieldConfig = {}): ZShieldInstance {\n const config = resolveConfig(options);\n const rateLimiter = new RateLimiter(config);\n const detector = new Detector({ rateLimiter, config });\n const blockManager = new BlockManager(config);\n const logger = new Logger(config);\n\n const middleware = createMiddleware(config, {\n rateLimiter,\n detector,\n blockManager,\n logger\n });\n\n return {\n middleware,\n config,\n rateLimiter,\n detector,\n blockManager,\n logger,\n\n // Programmatic Block API with security event logging\n block: (ip: string, duration?: string | number, reason?: string): BlockRecord => {\n const result = blockManager.block(ip, duration, reason);\n void logger.log({\n appName: config.appName,\n type: 'manual_block',\n severity: 'info',\n ip,\n method: 'INTERNAL',\n route: '/admin/block',\n reason: result.reason,\n action: 'blocked'\n });\n return result;\n },\n\n unblock: (ip: string): boolean => {\n const result = blockManager.unblock(ip);\n if (result) {\n void logger.log({\n appName: config.appName,\n type: 'manual_unblock',\n severity: 'info',\n ip,\n method: 'INTERNAL',\n route: '/admin/unblock',\n reason: 'Manual unblock requested',\n action: 'unblocked'\n });\n }\n return result;\n },\n\n isBlocked: (ip: string): boolean => blockManager.isBlocked(ip),\n getBlockInfo: (ip: string): BlockInfo | null => blockManager.getBlockInfo(ip),\n listBlocks: (): BlockInfo[] => blockManager.listBlocks(),\n flush: (): Promise<void> => logger.flush()\n };\n}\n\nexport {\n createMiddleware,\n extractClientIp,\n isRouteExcluded,\n safelyEndResponse,\n resolveConfig,\n DEFAULT_CONFIG,\n RateLimiter,\n Detector,\n BlockManager,\n isValidIp,\n normalizeIp,\n canonicalizeIpv6,\n parseDuration,\n Logger,\n stripAnsi,\n sanitizeString,\n sanitizeRoute,\n sanitizeMethod,\n createNextShield,\n withNextShield,\n extractNextClientIp\n};\n\nexport type { BlockInfo, ZShieldInstance, ZShieldFullInstance };\nexport * from './types';\n\n"]}
@@ -0,0 +1,141 @@
1
+ import type { ZShieldConfig, ZShieldOptions } from './types';
2
+ export interface LoggerOptions {
3
+ appName?: string;
4
+ logDir?: string;
5
+ logFilename?: string;
6
+ maxLogSizeBytes?: number;
7
+ maxLogFiles?: number;
8
+ maxLogQueueSize?: number;
9
+ maxQueueSize?: number;
10
+ onLogError?: ((err: Error, event: Record<string, unknown>) => void) | null;
11
+ }
12
+ export interface FormattedSecurityEvent {
13
+ timestamp: string;
14
+ appName: string;
15
+ type: string;
16
+ severity: string;
17
+ ip: string;
18
+ method: string;
19
+ route: string;
20
+ reason: string;
21
+ action: string;
22
+ }
23
+ /**
24
+ * Strips ANSI terminal escape codes from untrusted strings to prevent terminal injection.
25
+ * Removes CSI color/control sequences, OSC title/hyperlink sequences, and escape codes.
26
+ *
27
+ * @param str - Untrusted string
28
+ * @returns Clean string without ANSI escapes
29
+ */
30
+ export declare function stripAnsi(str: string): string;
31
+ /**
32
+ * Sanitizes an untrusted string to prevent Log Injection (CWE-117).
33
+ * - Strips ANSI escapes
34
+ * - Strips control characters (newlines \r, \n, null bytes, backspaces, tabs)
35
+ * - Truncates to maxLength to prevent disk space exhaustion
36
+ *
37
+ * @param val - Untrusted candidate value
38
+ * @param maxLength - Maximum permitted length
39
+ * @returns Sanitized string
40
+ */
41
+ export declare function sanitizeString(val: unknown, maxLength?: number): string;
42
+ /**
43
+ * Sanitizes HTTP route paths.
44
+ * @param route - Untrusted route string
45
+ * @param maxLength - Maximum permitted length
46
+ * @returns Sanitized route
47
+ */
48
+ export declare function sanitizeRoute(route: unknown, maxLength?: number): string;
49
+ /**
50
+ * Sanitizes HTTP methods.
51
+ * @param method - Untrusted method string
52
+ * @returns Sanitized uppercase HTTP method
53
+ */
54
+ export declare function sanitizeMethod(method: unknown): string;
55
+ /**
56
+ * Logger - Safe, Persistent Local JSON Lines Security Event Logger
57
+ *
58
+ * Design Features:
59
+ * - JSON Lines (.jsonl): Every event is on exactly one line ending in \n.
60
+ * - Non-Blocking & Asynchronous: Uses node:fs/promises without blocking the event loop.
61
+ * - Strict Sanitization: Eliminates Log Injection (CWE-117) by stripping newlines and ANSI escapes.
62
+ * - Privacy-Preserving: Never logs passwords, cookies, authorization headers, or request bodies (CWE-532).
63
+ * - Bounded Storage: Automatic log rotation and retention limits prevent disk space exhaustion.
64
+ * - Bounded Queue: Strictly caps pending in-memory write operations to prevent memory exhaustion under attack.
65
+ * - Resilient: Filesystem errors (ENOSPC, EACCES) are safely caught and will not crash the host app.
66
+ * - Sequential Queue: Guarantees concurrency safety so simultaneous writes never corrupt files.
67
+ *
68
+ * Hosting Notice:
69
+ * Logs are stored in local persistent storage. If deployed to ephemeral platforms
70
+ * (e.g. basic Heroku dynos, AWS Lambda, or serverless containers without mounted volumes),
71
+ * local log files may be lost upon container restart or redeployment.
72
+ */
73
+ export declare class Logger {
74
+ readonly appName: string;
75
+ readonly logDir: string;
76
+ readonly logFilename: string;
77
+ readonly filePath: string;
78
+ readonly maxLogSizeBytes: number;
79
+ readonly maxLogFiles: number;
80
+ readonly maxQueueSize: number;
81
+ readonly onLogError: ((err: Error, event: Record<string, unknown>) => void) | null;
82
+ private _writeQueue;
83
+ private _dirEnsured;
84
+ private _pendingWrites;
85
+ private _droppedEvents;
86
+ /**
87
+ * @param config - Logger options or resolved Z-Shield configuration
88
+ */
89
+ constructor(config?: LoggerOptions | ZShieldConfig | ZShieldOptions);
90
+ /**
91
+ * Returns the count of currently queued or executing asynchronous write operations.
92
+ */
93
+ get pendingWrites(): number;
94
+ /**
95
+ * Returns the cumulative count of log events dropped due to write queue overload.
96
+ */
97
+ get droppedEvents(): number;
98
+ /**
99
+ * Formats and sanitizes a security event into a validated schema.
100
+ * Deliberately omits request bodies, authorization headers, cookies, and tokens.
101
+ *
102
+ * @param rawEvent - Raw event object
103
+ * @returns Validated security event
104
+ */
105
+ formatEvent(rawEvent?: Record<string, unknown>): FormattedSecurityEvent;
106
+ /**
107
+ * Safely logs a security event asynchronously.
108
+ * Resolves to true if write succeeded, or false if it failed or was dropped due to queue overload.
109
+ *
110
+ * @param rawEvent - Raw event data
111
+ * @returns True if write succeeded, false otherwise
112
+ */
113
+ log(rawEvent: Record<string, unknown>): Promise<boolean>;
114
+ /**
115
+ * Waits for all currently queued asynchronous log writes to complete.
116
+ * Useful for deterministic testing, graceful shutdowns, and process exit handlers.
117
+ */
118
+ flush(): Promise<void>;
119
+ /**
120
+ * Internal write operation executed within the sequential queue.
121
+ *
122
+ * @param rawEvent - Raw event to serialize and write
123
+ */
124
+ private _writeEvent;
125
+ /**
126
+ * Ensures the target log directory exists.
127
+ */
128
+ private _ensureDir;
129
+ /**
130
+ * Checks file size and rotates log files if size exceeds maxLogSizeBytes.
131
+ */
132
+ private _rotateIfNeeded;
133
+ /**
134
+ * Safely handles write and filesystem failures without crashing the application.
135
+ *
136
+ * @param err - Encountered error
137
+ * @param rawEvent - Raw event data attempted to be written
138
+ */
139
+ private _handleError;
140
+ }
141
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
package/dist/logger.js ADDED
@@ -0,0 +1,347 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Logger = void 0;
7
+ exports.stripAnsi = stripAnsi;
8
+ exports.sanitizeString = sanitizeString;
9
+ exports.sanitizeRoute = sanitizeRoute;
10
+ exports.sanitizeMethod = sanitizeMethod;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const ANSI_REGEX = /[\u001B\u009B](?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
14
+ /**
15
+ * Strips ANSI terminal escape codes from untrusted strings to prevent terminal injection.
16
+ * Removes CSI color/control sequences, OSC title/hyperlink sequences, and escape codes.
17
+ *
18
+ * @param str - Untrusted string
19
+ * @returns Clean string without ANSI escapes
20
+ */
21
+ function stripAnsi(str) {
22
+ if (typeof str !== 'string') {
23
+ return '';
24
+ }
25
+ return str.replace(ANSI_REGEX, '');
26
+ }
27
+ /**
28
+ * Sanitizes an untrusted string to prevent Log Injection (CWE-117).
29
+ * - Strips ANSI escapes
30
+ * - Strips control characters (newlines \r, \n, null bytes, backspaces, tabs)
31
+ * - Truncates to maxLength to prevent disk space exhaustion
32
+ *
33
+ * @param val - Untrusted candidate value
34
+ * @param maxLength - Maximum permitted length
35
+ * @returns Sanitized string
36
+ */
37
+ function sanitizeString(val, maxLength = 250) {
38
+ if (val === null || val === undefined) {
39
+ return '';
40
+ }
41
+ let str = String(val);
42
+ str = stripAnsi(str);
43
+ // Replace control characters (ASCII 0-31, 127) with a clean space
44
+ str = str.replace(/[\x00-\x1f\x7f]/g, ' ').trim();
45
+ // Collapse consecutive spaces
46
+ str = str.replace(/\s+/g, ' ');
47
+ return str.slice(0, maxLength);
48
+ }
49
+ /**
50
+ * Sanitizes HTTP route paths.
51
+ * @param route - Untrusted route string
52
+ * @param maxLength - Maximum permitted length
53
+ * @returns Sanitized route
54
+ */
55
+ function sanitizeRoute(route, maxLength = 500) {
56
+ if (!route || typeof route !== 'string') {
57
+ return '/';
58
+ }
59
+ let clean = route.split(/[\r\n]/)[0] ?? '';
60
+ clean = stripAnsi(clean);
61
+ // Strip query parameters and URL fragments to prevent credential leakage (CWE-532)
62
+ clean = (clean.split('?')[0] ?? '').split('#')[0] ?? '';
63
+ // Remove dangerous control characters and newlines
64
+ clean = clean.replace(/[\x00-\x1f\x7f]/g, '').trim();
65
+ if (!clean.startsWith('/')) {
66
+ clean = '/' + clean;
67
+ }
68
+ return clean.slice(0, maxLength) || '/';
69
+ }
70
+ /**
71
+ * Sanitizes HTTP methods.
72
+ * @param method - Untrusted method string
73
+ * @returns Sanitized uppercase HTTP method
74
+ */
75
+ function sanitizeMethod(method) {
76
+ if (!method || typeof method !== 'string') {
77
+ return 'GET';
78
+ }
79
+ const clean = method.toUpperCase().replace(/[^A-Z]/g, '');
80
+ return clean.slice(0, 10) || 'GET';
81
+ }
82
+ /**
83
+ * Logger - Safe, Persistent Local JSON Lines Security Event Logger
84
+ *
85
+ * Design Features:
86
+ * - JSON Lines (.jsonl): Every event is on exactly one line ending in \n.
87
+ * - Non-Blocking & Asynchronous: Uses node:fs/promises without blocking the event loop.
88
+ * - Strict Sanitization: Eliminates Log Injection (CWE-117) by stripping newlines and ANSI escapes.
89
+ * - Privacy-Preserving: Never logs passwords, cookies, authorization headers, or request bodies (CWE-532).
90
+ * - Bounded Storage: Automatic log rotation and retention limits prevent disk space exhaustion.
91
+ * - Bounded Queue: Strictly caps pending in-memory write operations to prevent memory exhaustion under attack.
92
+ * - Resilient: Filesystem errors (ENOSPC, EACCES) are safely caught and will not crash the host app.
93
+ * - Sequential Queue: Guarantees concurrency safety so simultaneous writes never corrupt files.
94
+ *
95
+ * Hosting Notice:
96
+ * Logs are stored in local persistent storage. If deployed to ephemeral platforms
97
+ * (e.g. basic Heroku dynos, AWS Lambda, or serverless containers without mounted volumes),
98
+ * local log files may be lost upon container restart or redeployment.
99
+ */
100
+ class Logger {
101
+ appName;
102
+ logDir;
103
+ logFilename;
104
+ filePath;
105
+ maxLogSizeBytes;
106
+ maxLogFiles;
107
+ maxQueueSize;
108
+ onLogError;
109
+ _writeQueue;
110
+ _dirEnsured;
111
+ _pendingWrites;
112
+ _droppedEvents;
113
+ /**
114
+ * @param config - Logger options or resolved Z-Shield configuration
115
+ */
116
+ constructor(config = {}) {
117
+ this.appName = config.appName || 'default-app';
118
+ this.logDir = node_path_1.default.resolve(config.logDir || node_path_1.default.join(process.cwd(), '.z-shield-logs'));
119
+ this.logFilename = config.logFilename || 'events.jsonl';
120
+ this.filePath = node_path_1.default.join(this.logDir, this.logFilename);
121
+ this.maxLogSizeBytes = config.maxLogSizeBytes || 5 * 1024 * 1024; // 5MB
122
+ this.maxLogFiles = typeof config.maxLogFiles === 'number' ? config.maxLogFiles : 5;
123
+ const rawQueueLimit = config.maxLogQueueSize ??
124
+ config.maxQueueSize;
125
+ this.maxQueueSize =
126
+ typeof rawQueueLimit === 'number' && Number.isInteger(rawQueueLimit) && rawQueueLimit > 0
127
+ ? rawQueueLimit
128
+ : 1000;
129
+ this.onLogError = typeof config.onLogError === 'function' ? config.onLogError : null;
130
+ this._writeQueue = Promise.resolve();
131
+ this._dirEnsured = false;
132
+ this._pendingWrites = 0;
133
+ this._droppedEvents = 0;
134
+ }
135
+ /**
136
+ * Returns the count of currently queued or executing asynchronous write operations.
137
+ */
138
+ get pendingWrites() {
139
+ return this._pendingWrites;
140
+ }
141
+ /**
142
+ * Returns the cumulative count of log events dropped due to write queue overload.
143
+ */
144
+ get droppedEvents() {
145
+ return this._droppedEvents;
146
+ }
147
+ /**
148
+ * Formats and sanitizes a security event into a validated schema.
149
+ * Deliberately omits request bodies, authorization headers, cookies, and tokens.
150
+ *
151
+ * @param rawEvent - Raw event object
152
+ * @returns Validated security event
153
+ */
154
+ formatEvent(rawEvent = {}) {
155
+ const rawTimestamp = rawEvent.timestamp;
156
+ const now = typeof rawTimestamp === 'string' || typeof rawTimestamp === 'number'
157
+ ? new Date(rawTimestamp)
158
+ : new Date();
159
+ const timestamp = isNaN(now.getTime()) ? new Date().toISOString() : now.toISOString();
160
+ const allowedSeverities = ['info', 'warning', 'critical'];
161
+ const rawSeverity = typeof rawEvent.severity === 'string' ? rawEvent.severity : '';
162
+ const severity = allowedSeverities.includes(rawSeverity) ? rawSeverity : 'warning';
163
+ const allowedActions = ['blocked', 'warned', 'allowed', 'unblocked'];
164
+ const rawAction = typeof rawEvent.action === 'string' ? rawEvent.action : '';
165
+ const action = allowedActions.includes(rawAction) ? rawAction : 'warned';
166
+ return {
167
+ timestamp,
168
+ appName: sanitizeString(rawEvent.appName || this.appName, 50),
169
+ type: sanitizeString(rawEvent.type || 'security_event', 30),
170
+ severity,
171
+ ip: sanitizeString(rawEvent.ip || 'unknown', 45),
172
+ method: sanitizeMethod(rawEvent.method),
173
+ route: sanitizeRoute(rawEvent.route),
174
+ reason: sanitizeString(rawEvent.reason || 'Unspecified security event', 300),
175
+ action
176
+ };
177
+ }
178
+ /**
179
+ * Safely logs a security event asynchronously.
180
+ * Resolves to true if write succeeded, or false if it failed or was dropped due to queue overload.
181
+ *
182
+ * @param rawEvent - Raw event data
183
+ * @returns True if write succeeded, false otherwise
184
+ */
185
+ log(rawEvent) {
186
+ if (this._pendingWrites >= this.maxQueueSize) {
187
+ this._droppedEvents++;
188
+ if (this.onLogError) {
189
+ try {
190
+ this.onLogError(new Error(`[Z-Shield Logger]: Write queue capacity of ${this.maxQueueSize} reached. Event dropped.`), rawEvent);
191
+ }
192
+ catch {
193
+ // Prevent user callback exceptions from propagating or crashing
194
+ }
195
+ }
196
+ else if (this._droppedEvents === 1 || this._droppedEvents % 1000 === 0) {
197
+ console.error(`[Z-Shield Logger Warning]: Write queue capacity of ${this.maxQueueSize} reached. ${this._droppedEvents} event(s) dropped.`);
198
+ }
199
+ return Promise.resolve(false);
200
+ }
201
+ this._pendingWrites++;
202
+ return new Promise((resolve) => {
203
+ this._writeQueue = this._writeQueue
204
+ .then(() => this._writeEvent(rawEvent))
205
+ .then(() => {
206
+ this._pendingWrites--;
207
+ resolve(true);
208
+ })
209
+ .catch((err) => {
210
+ this._pendingWrites--;
211
+ this._handleError(err, rawEvent);
212
+ resolve(false);
213
+ });
214
+ });
215
+ }
216
+ /**
217
+ * Waits for all currently queued asynchronous log writes to complete.
218
+ * Useful for deterministic testing, graceful shutdowns, and process exit handlers.
219
+ */
220
+ async flush() {
221
+ await this._writeQueue;
222
+ }
223
+ /**
224
+ * Internal write operation executed within the sequential queue.
225
+ *
226
+ * @param rawEvent - Raw event to serialize and write
227
+ */
228
+ async _writeEvent(rawEvent) {
229
+ const event = this.formatEvent(rawEvent);
230
+ await this._ensureDir();
231
+ await this._rotateIfNeeded();
232
+ const line = JSON.stringify(event) + '\n';
233
+ try {
234
+ await node_fs_1.default.promises.appendFile(this.filePath, line, { encoding: 'utf8', flag: 'a' });
235
+ }
236
+ catch (err) {
237
+ // Self-healing recovery: if directory was removed externally right before write,
238
+ // recreate directory and retry appending once before throwing.
239
+ if (err.code === 'ENOENT') {
240
+ this._dirEnsured = false;
241
+ await node_fs_1.default.promises.mkdir(this.logDir, { recursive: true });
242
+ this._dirEnsured = true;
243
+ await node_fs_1.default.promises.appendFile(this.filePath, line, { encoding: 'utf8', flag: 'a' });
244
+ return;
245
+ }
246
+ throw err;
247
+ }
248
+ }
249
+ /**
250
+ * Ensures the target log directory exists.
251
+ */
252
+ async _ensureDir() {
253
+ if (!this._dirEnsured || !node_fs_1.default.existsSync(this.logDir)) {
254
+ await node_fs_1.default.promises.mkdir(this.logDir, { recursive: true });
255
+ this._dirEnsured = true;
256
+ }
257
+ }
258
+ /**
259
+ * Checks file size and rotates log files if size exceeds maxLogSizeBytes.
260
+ */
261
+ async _rotateIfNeeded() {
262
+ try {
263
+ const stats = await node_fs_1.default.promises.stat(this.filePath);
264
+ if (stats.size < this.maxLogSizeBytes) {
265
+ return;
266
+ }
267
+ }
268
+ catch (err) {
269
+ if (err.code === 'ENOENT') {
270
+ return; // File does not exist yet
271
+ }
272
+ throw err;
273
+ }
274
+ const { name, ext } = node_path_1.default.parse(this.logFilename);
275
+ // If maxLogFiles is 0, simply remove the current file
276
+ if (this.maxLogFiles === 0) {
277
+ try {
278
+ await node_fs_1.default.promises.unlink(this.filePath);
279
+ }
280
+ catch (err) {
281
+ if (err.code !== 'ENOENT')
282
+ throw err;
283
+ }
284
+ return;
285
+ }
286
+ // Shift existing rotated files:
287
+ // e.g. events.5.jsonl deleted (retention limit)
288
+ // events.4.jsonl -> events.5.jsonl
289
+ // events.1.jsonl -> events.2.jsonl
290
+ for (let i = this.maxLogFiles; i >= 1; i--) {
291
+ const currentRotated = node_path_1.default.join(this.logDir, `${name}.${i}${ext}`);
292
+ if (i === this.maxLogFiles) {
293
+ try {
294
+ await node_fs_1.default.promises.unlink(currentRotated);
295
+ }
296
+ catch (err) {
297
+ if (err.code !== 'ENOENT')
298
+ throw err;
299
+ }
300
+ }
301
+ else {
302
+ const nextRotated = node_path_1.default.join(this.logDir, `${name}.${i + 1}${ext}`);
303
+ try {
304
+ await node_fs_1.default.promises.rename(currentRotated, nextRotated);
305
+ }
306
+ catch (err) {
307
+ if (err.code !== 'ENOENT')
308
+ throw err;
309
+ }
310
+ }
311
+ }
312
+ // Move current events.jsonl to events.1.jsonl
313
+ const firstRotated = node_path_1.default.join(this.logDir, `${name}.1${ext}`);
314
+ try {
315
+ await node_fs_1.default.promises.rename(this.filePath, firstRotated);
316
+ }
317
+ catch (err) {
318
+ if (err.code !== 'ENOENT')
319
+ throw err;
320
+ }
321
+ }
322
+ /**
323
+ * Safely handles write and filesystem failures without crashing the application.
324
+ *
325
+ * @param err - Encountered error
326
+ * @param rawEvent - Raw event data attempted to be written
327
+ */
328
+ _handleError(err, rawEvent) {
329
+ // If the directory was deleted externally while running, reset _dirEnsured so the next
330
+ // write attempt will automatically recreate the directory instead of failing permanently.
331
+ this._dirEnsured = false;
332
+ if (this.onLogError) {
333
+ try {
334
+ this.onLogError(err, rawEvent);
335
+ }
336
+ catch {
337
+ // Prevent errors in the user's callback from crashing the app
338
+ }
339
+ }
340
+ else {
341
+ // Print clear, non-fatal warning to stderr
342
+ console.error(`[Z-Shield Logger Warning]: Failed to write security event log: ${err.message}`);
343
+ }
344
+ }
345
+ }
346
+ exports.Logger = Logger;
347
+ //# sourceMappingURL=logger.js.map