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/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"]}
|
package/dist/logger.d.ts
ADDED
|
@@ -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":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE7D,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;CAC5E;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAKD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,CAW5E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,CAc7E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAMtD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,MAAM;IACjB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAEnF,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAS;IAE/B;;OAEG;IACH,YAAY,MAAM,GAAE,aAAa,GAAG,aAAa,GAAG,cAAmB,EAoBtE;IAED;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,sBAAsB,CA0B1E;IAED;;;;;;OAMG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAkCvD;IAED;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAED;;;;OAIG;YACW,WAAW;IAsBzB;;OAEG;YACW,UAAU;IAOxB;;OAEG;YACW,eAAe;IAwD7B;;;;;OAKG;IACH,OAAO,CAAC,YAAY;CAgBrB"}
|
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
|