@dimzxzzx07/file-watcher 1.0.0
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/.env +13 -0
- package/.eslintrc.json +128 -0
- package/.prettierrc +18 -0
- package/Dimzxzzx07.png +0 -0
- package/README.md +1024 -0
- package/dist/core/BackupManager.d.ts +25 -0
- package/dist/core/BackupManager.d.ts.map +1 -0
- package/dist/core/BackupManager.js +290 -0
- package/dist/core/BackupManager.js.map +1 -0
- package/dist/core/IntegrityValidator.d.ts +18 -0
- package/dist/core/IntegrityValidator.d.ts.map +1 -0
- package/dist/core/IntegrityValidator.js +212 -0
- package/dist/core/IntegrityValidator.js.map +1 -0
- package/dist/core/SecurityManager.d.ts +40 -0
- package/dist/core/SecurityManager.d.ts.map +1 -0
- package/dist/core/SecurityManager.js +320 -0
- package/dist/core/SecurityManager.js.map +1 -0
- package/dist/core/WatcherEngine.d.ts +44 -0
- package/dist/core/WatcherEngine.d.ts.map +1 -0
- package/dist/core/WatcherEngine.js +470 -0
- package/dist/core/WatcherEngine.js.map +1 -0
- package/dist/crypto/HashGenerator.d.ts +26 -0
- package/dist/crypto/HashGenerator.d.ts.map +1 -0
- package/dist/crypto/HashGenerator.js +220 -0
- package/dist/crypto/HashGenerator.js.map +1 -0
- package/dist/crypto/KeyManager.d.ts +30 -0
- package/dist/crypto/KeyManager.d.ts.map +1 -0
- package/dist/crypto/KeyManager.js +235 -0
- package/dist/crypto/KeyManager.js.map +1 -0
- package/dist/crypto/SignatureValidator.d.ts +11 -0
- package/dist/crypto/SignatureValidator.d.ts.map +1 -0
- package/dist/crypto/SignatureValidator.js +102 -0
- package/dist/crypto/SignatureValidator.js.map +1 -0
- package/dist/detectors/AnomalyDetector.d.ts +24 -0
- package/dist/detectors/AnomalyDetector.d.ts.map +1 -0
- package/dist/detectors/AnomalyDetector.js +209 -0
- package/dist/detectors/AnomalyDetector.js.map +1 -0
- package/dist/detectors/InjectionDetector.d.ts +14 -0
- package/dist/detectors/InjectionDetector.d.ts.map +1 -0
- package/dist/detectors/InjectionDetector.js +204 -0
- package/dist/detectors/InjectionDetector.js.map +1 -0
- package/dist/detectors/PatternMatcher.d.ts +28 -0
- package/dist/detectors/PatternMatcher.d.ts.map +1 -0
- package/dist/detectors/PatternMatcher.js +283 -0
- package/dist/detectors/PatternMatcher.js.map +1 -0
- package/dist/guards/FileGuard.d.ts +35 -0
- package/dist/guards/FileGuard.d.ts.map +1 -0
- package/dist/guards/FileGuard.js +357 -0
- package/dist/guards/FileGuard.js.map +1 -0
- package/dist/guards/MemoryGuard.d.ts +28 -0
- package/dist/guards/MemoryGuard.d.ts.map +1 -0
- package/dist/guards/MemoryGuard.js +256 -0
- package/dist/guards/MemoryGuard.js.map +1 -0
- package/dist/guards/ProcessGuard.d.ts +25 -0
- package/dist/guards/ProcessGuard.d.ts.map +1 -0
- package/dist/guards/ProcessGuard.js +221 -0
- package/dist/guards/ProcessGuard.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +69 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/Constants.d.ts +407 -0
- package/dist/utils/Constants.d.ts.map +1 -0
- package/dist/utils/Constants.js +505 -0
- package/dist/utils/Constants.js.map +1 -0
- package/dist/utils/Logger.d.ts +45 -0
- package/dist/utils/Logger.d.ts.map +1 -0
- package/dist/utils/Logger.js +285 -0
- package/dist/utils/Logger.js.map +1 -0
- package/dist/utils/Validator.d.ts +27 -0
- package/dist/utils/Validator.d.ts.map +1 -0
- package/dist/utils/Validator.js +245 -0
- package/dist/utils/Validator.js.map +1 -0
- package/favicon.png +0 -0
- package/jest.config.js +69 -0
- package/package.json +69 -0
- package/src/core/BackupManager.ts +305 -0
- package/src/core/IntegrityValidator.ts +200 -0
- package/src/core/SecurityManager.ts +348 -0
- package/src/core/WatcherEngine.ts +537 -0
- package/src/crypto/HashGenerator.ts +234 -0
- package/src/crypto/KeyManager.ts +249 -0
- package/src/crypto/SignatureValidator.ts +76 -0
- package/src/detectors/AnomalyDetector.ts +247 -0
- package/src/detectors/InjectionDetector.ts +233 -0
- package/src/detectors/PatternMatcher.ts +319 -0
- package/src/guards/FileGuard.ts +385 -0
- package/src/guards/MemoryGuard.ts +263 -0
- package/src/guards/ProcessGuard.ts +219 -0
- package/src/index.ts +189 -0
- package/src/types/index.ts +72 -0
- package/src/utils/Constants.ts +532 -0
- package/src/utils/Logger.ts +279 -0
- package/src/utils/Validator.ts +248 -0
- package/tests/setup.ts +80 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Logger } from '../utils/Logger';
|
|
2
|
+
|
|
3
|
+
export class PatternMatcher {
|
|
4
|
+
private readonly logger: Logger;
|
|
5
|
+
private readonly patterns: Map<string, RegExp[]>;
|
|
6
|
+
private readonly signatures: Map<string, string[]>;
|
|
7
|
+
private readonly heuristics: Map<string, Function>;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.logger = Logger.getInstance();
|
|
11
|
+
this.patterns = new Map();
|
|
12
|
+
this.signatures = new Map();
|
|
13
|
+
this.heuristics = new Map();
|
|
14
|
+
|
|
15
|
+
this.initializePatterns();
|
|
16
|
+
this.initializeSignatures();
|
|
17
|
+
this.initializeHeuristics();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private initializePatterns(): void {
|
|
21
|
+
this.patterns.set('injection', [
|
|
22
|
+
/eval\s*\(\s*['"`][^)]*['"`]\s*\)/gi,
|
|
23
|
+
/Function\s*\(\s*['"`][^)]*['"`]\s*\)/gi,
|
|
24
|
+
/setTimeout\s*\(\s*['"`][^)]*['"`]\s*\)/gi,
|
|
25
|
+
/setInterval\s*\(\s*['"`][^)]*['"`]\s*\)/gi,
|
|
26
|
+
/new\s+Function\s*\(\s*['"`][^)]*['"`]\s*\)/gi,
|
|
27
|
+
/require\s*\(\s*['"`](?:fs|child_process|vm|cluster)['"`]\s*\)/gi,
|
|
28
|
+
/process\.(?:binding|dlopen|kill)\s*\(/gi,
|
|
29
|
+
/child_process\.(?:exec|spawn|fork)\s*\(/gi,
|
|
30
|
+
/vm\.(?:runIn|create)\s*\(/gi
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
this.patterns.set('obfuscation', [
|
|
34
|
+
/String\.fromCharCode\s*\([^)]+\)/gi,
|
|
35
|
+
/unescape\s*\([^)]+\)/gi,
|
|
36
|
+
/escape\s*\([^)]+\)/gi,
|
|
37
|
+
/decodeURI(?:Component)?\s*\([^)]+\)/gi,
|
|
38
|
+
/atob\s*\([^)]+\)/gi,
|
|
39
|
+
/btoa\s*\([^)]+\)/gi,
|
|
40
|
+
/Buffer\.from\s*\([^)]+\)/gi,
|
|
41
|
+
/new\s+Buffer\s*\([^)]+\)/gi,
|
|
42
|
+
/\[[A-Za-z0-9+/]{50,}\]/g,
|
|
43
|
+
/\\x[0-9a-f]{2,50}/gi
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
this.patterns.set('backdoor', [
|
|
47
|
+
/net\.(?:createServer|connect)\s*\(/gi,
|
|
48
|
+
/http\.(?:createServer|request)\s*\(/gi,
|
|
49
|
+
/tls\.(?:createServer|connect)\s*\(/gi,
|
|
50
|
+
/dgram\.createSocket\s*\(/gi,
|
|
51
|
+
/WebSocket\s*\(/gi,
|
|
52
|
+
/Socket\.(?:connect|bind|listen)\s*\(/gi,
|
|
53
|
+
/server\.listen\s*\(\s*(?:[0-9]+|['"`][^)]*['"`])\s*\)/gi
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
this.patterns.set('malware', [
|
|
57
|
+
/crypto\.(?:miner|monero)/gi,
|
|
58
|
+
/xmr\./gi,
|
|
59
|
+
/coinhive/gi,
|
|
60
|
+
/Crypt(?:o)?Night/gi,
|
|
61
|
+
/webchain/gi,
|
|
62
|
+
/miner\./gi
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
this.patterns.set('anti-debug', [
|
|
66
|
+
/debugger;/g,
|
|
67
|
+
/--inspect/g,
|
|
68
|
+
/--debug/g,
|
|
69
|
+
/process\.(?:_getActiveRequests|_getActiveHandles)/g,
|
|
70
|
+
/Error\.captureStackTrace/g,
|
|
71
|
+
/process\.binding\(['"`]debug['"`]\)/g
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
this.patterns.set('self-modify', [
|
|
75
|
+
/fs\.(?:writeFile|unlink|rename)Sync?\s*\(\s*__filename/gi,
|
|
76
|
+
/fs\.(?:writeFile|unlink|rename)Sync?\s*\(\s*module\.filename/gi,
|
|
77
|
+
/process\.argv\s*\[\s*1\s*\]/gi,
|
|
78
|
+
/module\.exports\s*=\s*\{/gi,
|
|
79
|
+
/exports\.[a-zA-Z_]+\s*=/gi
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private initializeSignatures(): void {
|
|
84
|
+
this.signatures.set('malware', [
|
|
85
|
+
'4d5a90',
|
|
86
|
+
'7f454c46',
|
|
87
|
+
'cafebabe',
|
|
88
|
+
'1f8b08',
|
|
89
|
+
'504b0304',
|
|
90
|
+
'25504446',
|
|
91
|
+
'd0cf11e0',
|
|
92
|
+
'38425053'
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
this.signatures.set('injection', [
|
|
96
|
+
'eval(atob(',
|
|
97
|
+
'new Function(atob(',
|
|
98
|
+
'process.binding',
|
|
99
|
+
'Reflect.construct',
|
|
100
|
+
'Object.defineProperty',
|
|
101
|
+
'__defineGetter__',
|
|
102
|
+
'__defineSetter__'
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
this.signatures.set('backdoor', [
|
|
106
|
+
'reverse shell',
|
|
107
|
+
'bind shell',
|
|
108
|
+
'backconnect',
|
|
109
|
+
'command injection',
|
|
110
|
+
'remote access'
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private initializeHeuristics(): void {
|
|
115
|
+
this.heuristics.set('suspicious_structure', (content: string) => {
|
|
116
|
+
let score = 0;
|
|
117
|
+
|
|
118
|
+
const tryCount = (content.match(/try\s*{/g) || []).length;
|
|
119
|
+
const catchCount = (content.match(/catch\s*\(/g) || []).length;
|
|
120
|
+
if (tryCount > catchCount) score += 10;
|
|
121
|
+
|
|
122
|
+
const concatCount = (content.match(/\+/g) || []).length;
|
|
123
|
+
if (concatCount > 100) score += 20;
|
|
124
|
+
|
|
125
|
+
const functionCount = (content.match(/function\s*\(/g) || []).length;
|
|
126
|
+
if (functionCount > 20) score += 15;
|
|
127
|
+
|
|
128
|
+
return score > 30;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.heuristics.set('weird_variables', (content: string) => {
|
|
132
|
+
const varPattern = /(?:var|let|const)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)/g;
|
|
133
|
+
const matches = content.match(varPattern) || [];
|
|
134
|
+
|
|
135
|
+
let weirdCount = 0;
|
|
136
|
+
for (const match of matches) {
|
|
137
|
+
const varName = match.split(/\s+/)[1];
|
|
138
|
+
if (varName && (
|
|
139
|
+
varName.length > 30 ||
|
|
140
|
+
/^_+$/.test(varName) ||
|
|
141
|
+
/^[0-9]+$/.test(varName) ||
|
|
142
|
+
varName.includes('_0x')
|
|
143
|
+
)) {
|
|
144
|
+
weirdCount++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return weirdCount > 5;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.heuristics.set('suspicious_strings', (content: string) => {
|
|
152
|
+
const suspiciousStrings = [
|
|
153
|
+
'eval', 'Function', 'constructor', 'prototype',
|
|
154
|
+
'__proto__', 'defineProperty', 'getOwnProperty',
|
|
155
|
+
'caller', 'callee', 'arguments', 'apply', 'bind',
|
|
156
|
+
'toString', 'valueOf', 'hasOwnProperty',
|
|
157
|
+
'isPrototypeOf', 'propertyIsEnumerable',
|
|
158
|
+
'toLocaleString', 'watch', 'unwatch'
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
let suspiciousCount = 0;
|
|
162
|
+
for (const str of suspiciousStrings) {
|
|
163
|
+
const regex = new RegExp(`['"\`]${str}['"\`]`, 'g');
|
|
164
|
+
const matches = content.match(regex) || [];
|
|
165
|
+
suspiciousCount += matches.length;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return suspiciousCount > 10;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.heuristics.set('encoded_content', (content: string) => {
|
|
172
|
+
const base64Matches = content.match(/[A-Za-z0-9+/]{100,}={0,2}/g) || [];
|
|
173
|
+
const hexMatches = content.match(/[0-9a-fA-F]{100,}/g) || [];
|
|
174
|
+
const unicodeMatches = content.match(/\\u[0-9a-fA-F]{4}/g) || [];
|
|
175
|
+
|
|
176
|
+
return base64Matches.length > 0 ||
|
|
177
|
+
hexMatches.length > 0 ||
|
|
178
|
+
unicodeMatches.length > 20;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public matchPatterns(content: string, type: string): boolean {
|
|
183
|
+
const patterns = this.patterns.get(type);
|
|
184
|
+
if (!patterns) return false;
|
|
185
|
+
|
|
186
|
+
for (const pattern of patterns) {
|
|
187
|
+
if (pattern.test(content)) {
|
|
188
|
+
this.logger.debug(`Pattern matched: ${type}`, { pattern: pattern.source });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public matchSignatures(content: string, type: string): boolean {
|
|
197
|
+
const signatures = this.signatures.get(type);
|
|
198
|
+
if (!signatures) return false;
|
|
199
|
+
|
|
200
|
+
for (const signature of signatures) {
|
|
201
|
+
if (content.includes(signature)) {
|
|
202
|
+
this.logger.debug(`Signature matched: ${type}`, { signature });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public applyHeuristics(content: string): Map<string, boolean> {
|
|
211
|
+
const results = new Map<string, boolean>();
|
|
212
|
+
|
|
213
|
+
for (const [name, heuristic] of this.heuristics) {
|
|
214
|
+
try {
|
|
215
|
+
const result = heuristic(content);
|
|
216
|
+
results.set(name, result);
|
|
217
|
+
|
|
218
|
+
if (result) {
|
|
219
|
+
this.logger.debug(`Heuristic triggered: ${name}`);
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.logger.error(`Heuristic failed: ${name}`, { error });
|
|
223
|
+
results.set(name, false);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public comprehensiveScan(content: string): {
|
|
231
|
+
matched: boolean;
|
|
232
|
+
findings: Array<{ type: string; pattern: string; severity: string }>;
|
|
233
|
+
score: number;
|
|
234
|
+
} {
|
|
235
|
+
const findings: Array<{ type: string; pattern: string; severity: string }> = [];
|
|
236
|
+
let totalScore = 0;
|
|
237
|
+
|
|
238
|
+
for (const [type, patterns] of this.patterns) {
|
|
239
|
+
for (const pattern of patterns) {
|
|
240
|
+
const matches = content.match(new RegExp(pattern.source, 'g')) || [];
|
|
241
|
+
if (matches.length > 0) {
|
|
242
|
+
findings.push({
|
|
243
|
+
type,
|
|
244
|
+
pattern: pattern.source,
|
|
245
|
+
severity: this.getSeverity(type, matches.length)
|
|
246
|
+
});
|
|
247
|
+
totalScore += matches.length * this.getWeight(type);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const [type, signatures] of this.signatures) {
|
|
253
|
+
for (const signature of signatures) {
|
|
254
|
+
if (content.includes(signature)) {
|
|
255
|
+
findings.push({
|
|
256
|
+
type,
|
|
257
|
+
pattern: signature,
|
|
258
|
+
severity: 'high'
|
|
259
|
+
});
|
|
260
|
+
totalScore += 20;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const heuristicResults = this.applyHeuristics(content);
|
|
266
|
+
for (const [name, result] of heuristicResults) {
|
|
267
|
+
if (result) {
|
|
268
|
+
findings.push({
|
|
269
|
+
type: 'heuristic',
|
|
270
|
+
pattern: name,
|
|
271
|
+
severity: 'medium'
|
|
272
|
+
});
|
|
273
|
+
totalScore += 10;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
matched: findings.length > 0,
|
|
279
|
+
findings,
|
|
280
|
+
score: totalScore
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private getSeverity(type: string, count: number): string {
|
|
285
|
+
if (type === 'malware' || type === 'backdoor') return 'critical';
|
|
286
|
+
if (type === 'injection') return 'high';
|
|
287
|
+
if (type === 'anti-debug' || type === 'self-modify') return 'high';
|
|
288
|
+
if (type === 'obfuscation') return 'medium';
|
|
289
|
+
|
|
290
|
+
if (count > 10) return 'high';
|
|
291
|
+
if (count > 5) return 'medium';
|
|
292
|
+
return 'low';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private getWeight(type: string): number {
|
|
296
|
+
const weights: Record<string, number> = {
|
|
297
|
+
'malware': 10,
|
|
298
|
+
'backdoor': 10,
|
|
299
|
+
'injection': 8,
|
|
300
|
+
'anti-debug': 6,
|
|
301
|
+
'self-modify': 6,
|
|
302
|
+
'obfuscation': 4
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
return weights[type] || 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public getPatterns(): string[] {
|
|
309
|
+
return Array.from(this.patterns.keys());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
public getSignatures(): string[] {
|
|
313
|
+
return Array.from(this.signatures.keys());
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
public getHeuristics(): string[] {
|
|
317
|
+
return Array.from(this.heuristics.keys());
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { Logger } from '../utils/Logger';
|
|
5
|
+
|
|
6
|
+
export class FileGuard {
|
|
7
|
+
private static instance: FileGuard;
|
|
8
|
+
private readonly logger: Logger;
|
|
9
|
+
private readonly fileLocks: Map<string, { locked: boolean; pid: number }>;
|
|
10
|
+
private readonly filePermissions: Map<string, number>;
|
|
11
|
+
private readonly watchedFiles: Set<string>;
|
|
12
|
+
private readonly quarantineDir: string;
|
|
13
|
+
private isActive: boolean = true;
|
|
14
|
+
|
|
15
|
+
private constructor() {
|
|
16
|
+
this.logger = Logger.getInstance();
|
|
17
|
+
this.fileLocks = new Map();
|
|
18
|
+
this.filePermissions = new Map();
|
|
19
|
+
this.watchedFiles = new Set();
|
|
20
|
+
this.quarantineDir = path.join(process.cwd(), '.quarantine');
|
|
21
|
+
|
|
22
|
+
this.initializeQuarantine();
|
|
23
|
+
this.startFileMonitoring();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public static getInstance(): FileGuard {
|
|
27
|
+
if (!FileGuard.instance) {
|
|
28
|
+
FileGuard.instance = new FileGuard();
|
|
29
|
+
}
|
|
30
|
+
return FileGuard.instance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async initializeQuarantine(): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
await fs.mkdir(this.quarantineDir, { recursive: true, mode: 0o700 });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.logger.error('Failed to create quarantine directory', { error });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private startFileMonitoring(): void {
|
|
42
|
+
setInterval(() => {
|
|
43
|
+
this.checkFileIntegrity();
|
|
44
|
+
}, 10000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async protectFile(filePath: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
if (!await this.fileExists(filePath)) {
|
|
50
|
+
this.logger.warning(`File not found: ${filePath}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stats = await fs.stat(filePath);
|
|
55
|
+
|
|
56
|
+
this.filePermissions.set(filePath, stats.mode);
|
|
57
|
+
|
|
58
|
+
await fs.chmod(filePath, 0o444);
|
|
59
|
+
|
|
60
|
+
this.watchedFiles.add(filePath);
|
|
61
|
+
|
|
62
|
+
await this.createBackup(filePath);
|
|
63
|
+
|
|
64
|
+
await this.calculateFileHash(filePath);
|
|
65
|
+
this.fileLocks.set(filePath, { locked: true, pid: process.pid });
|
|
66
|
+
|
|
67
|
+
this.logger.info(`File protected: ${filePath}`);
|
|
68
|
+
return true;
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.logger.error(`Failed to protect file: ${filePath}`, { error });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async lockFile(filePath: string): Promise<boolean> {
|
|
77
|
+
try {
|
|
78
|
+
const lock = this.fileLocks.get(filePath);
|
|
79
|
+
if (lock && lock.pid !== process.pid) {
|
|
80
|
+
this.logger.warning(`File already locked by process ${lock.pid}: ${filePath}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await fs.chmod(filePath, 0o444);
|
|
85
|
+
|
|
86
|
+
this.fileLocks.set(filePath, { locked: true, pid: process.pid });
|
|
87
|
+
|
|
88
|
+
this.logger.debug(`File locked: ${filePath}`);
|
|
89
|
+
return true;
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.logger.error(`Failed to lock file: ${filePath}`, { error });
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async unlockFile(filePath: string): Promise<boolean> {
|
|
98
|
+
try {
|
|
99
|
+
const lock = this.fileLocks.get(filePath);
|
|
100
|
+
if (lock && lock.pid !== process.pid) {
|
|
101
|
+
this.logger.warning(`File locked by different process: ${filePath}`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const originalMode = this.filePermissions.get(filePath) || 0o644;
|
|
106
|
+
await fs.chmod(filePath, originalMode);
|
|
107
|
+
|
|
108
|
+
this.fileLocks.delete(filePath);
|
|
109
|
+
|
|
110
|
+
this.logger.debug(`File unlocked: ${filePath}`);
|
|
111
|
+
return true;
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.logger.error(`Failed to unlock file: ${filePath}`, { error });
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async quarantineFile(filePath: string, reason: string): Promise<string> {
|
|
120
|
+
try {
|
|
121
|
+
const quarantineName = `${path.basename(filePath)}.${Date.now()}.quarantine`;
|
|
122
|
+
const quarantinePath = path.join(this.quarantineDir, quarantineName);
|
|
123
|
+
|
|
124
|
+
await fs.copyFile(filePath, quarantinePath);
|
|
125
|
+
|
|
126
|
+
await fs.chmod(quarantinePath, 0o400);
|
|
127
|
+
|
|
128
|
+
const metadata = {
|
|
129
|
+
originalPath: filePath,
|
|
130
|
+
quarantinedAt: new Date().toISOString(),
|
|
131
|
+
reason,
|
|
132
|
+
pid: process.pid,
|
|
133
|
+
hash: await this.calculateFileHash(filePath)
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const metadataPath = quarantinePath + '.meta';
|
|
137
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
138
|
+
|
|
139
|
+
this.logger.warning(`File quarantined: ${filePath} -> ${quarantinePath}`);
|
|
140
|
+
|
|
141
|
+
return quarantinePath;
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
this.logger.error(`Failed to quarantine file: ${filePath}`, { error });
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async restoreFromQuarantine(quarantinePath: string): Promise<boolean> {
|
|
150
|
+
try {
|
|
151
|
+
const metadataPath = quarantinePath + '.meta';
|
|
152
|
+
const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
|
|
153
|
+
|
|
154
|
+
await fs.copyFile(quarantinePath, metadata.originalPath);
|
|
155
|
+
|
|
156
|
+
await fs.chmod(metadata.originalPath, 0o644);
|
|
157
|
+
|
|
158
|
+
this.logger.info(`File restored from quarantine: ${metadata.originalPath}`);
|
|
159
|
+
return true;
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.logger.error(`Failed to restore from quarantine: ${quarantinePath}`, { error });
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public async verifyFileIntegrity(filePath: string): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
if (!await this.fileExists(filePath)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const lock = this.fileLocks.get(filePath);
|
|
174
|
+
if (lock && lock.locked) {
|
|
175
|
+
if (lock.pid !== process.pid) {
|
|
176
|
+
this.logger.warning(`File locked by different process: ${filePath}`);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const stats = await fs.stat(filePath);
|
|
182
|
+
if ((stats.mode & 0o222) !== 0) {
|
|
183
|
+
this.logger.warning(`File is writable: ${filePath}`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const currentHash = await this.calculateFileHash(filePath);
|
|
188
|
+
const storedHash = await this.getStoredHash(filePath);
|
|
189
|
+
|
|
190
|
+
if (storedHash && currentHash !== storedHash) {
|
|
191
|
+
this.logger.warning(`File hash mismatch: ${filePath}`);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return true;
|
|
196
|
+
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.logger.error(`Integrity check failed: ${filePath}`, { error });
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public async createBackup(filePath: string): Promise<string> {
|
|
204
|
+
const backupDir = path.join(process.cwd(), '.backups');
|
|
205
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
const backupName = `${path.basename(filePath)}.${Date.now()}.backup`;
|
|
208
|
+
const backupPath = path.join(backupDir, backupName);
|
|
209
|
+
|
|
210
|
+
await fs.copyFile(filePath, backupPath);
|
|
211
|
+
|
|
212
|
+
const hash = await this.calculateFileHash(filePath);
|
|
213
|
+
const hashPath = backupPath + '.hash';
|
|
214
|
+
await fs.writeFile(hashPath, hash);
|
|
215
|
+
|
|
216
|
+
return backupPath;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public async restoreFromBackup(backupPath: string, targetPath: string): Promise<boolean> {
|
|
220
|
+
try {
|
|
221
|
+
const hashPath = backupPath + '.hash';
|
|
222
|
+
const storedHash = await fs.readFile(hashPath, 'utf8');
|
|
223
|
+
const currentHash = await this.calculateFileHash(backupPath);
|
|
224
|
+
|
|
225
|
+
if (currentHash !== storedHash) {
|
|
226
|
+
this.logger.error(`Backup integrity check failed: ${backupPath}`);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await fs.copyFile(backupPath, targetPath);
|
|
231
|
+
|
|
232
|
+
this.logger.info(`File restored from backup: ${targetPath}`);
|
|
233
|
+
return true;
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
this.logger.error(`Failed to restore from backup: ${backupPath}`, { error });
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public async monitorFile(filePath: string): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
const watcher = require('fs').watch(filePath, (eventType: string) => {
|
|
244
|
+
if (eventType === 'change') {
|
|
245
|
+
this.handleFileChange(filePath);
|
|
246
|
+
} else if (eventType === 'rename') {
|
|
247
|
+
this.handleFileDelete(filePath);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.logger.debug(`Monitoring file: ${filePath}`);
|
|
252
|
+
|
|
253
|
+
(global as any).fileWatchers = (global as any).fileWatchers || new Map();
|
|
254
|
+
(global as any).fileWatchers.set(filePath, watcher);
|
|
255
|
+
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.logger.error(`Failed to monitor file: ${filePath}`, { error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async handleFileChange(filePath: string): Promise<void> {
|
|
262
|
+
this.logger.info(`File changed: ${filePath}`);
|
|
263
|
+
|
|
264
|
+
const isValid = await this.verifyFileIntegrity(filePath);
|
|
265
|
+
|
|
266
|
+
if (!isValid) {
|
|
267
|
+
this.logger.critical(`File integrity compromised: ${filePath}`);
|
|
268
|
+
await this.quarantineFile(filePath, 'integrity_violation');
|
|
269
|
+
|
|
270
|
+
const backups = await this.findBackups(filePath);
|
|
271
|
+
if (backups.length > 0) {
|
|
272
|
+
await this.restoreFromBackup(backups[0] as string, filePath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async handleFileDelete(filePath: string): Promise<void> {
|
|
278
|
+
this.logger.warning(`File deleted: ${filePath}`);
|
|
279
|
+
|
|
280
|
+
const backups = await this.findBackups(filePath);
|
|
281
|
+
if (backups.length > 0) {
|
|
282
|
+
await this.restoreFromBackup(backups[0] as string, filePath);
|
|
283
|
+
this.logger.info(`File restored after deletion: ${filePath}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async checkFileIntegrity(): Promise<void> {
|
|
288
|
+
for (const filePath of this.watchedFiles) {
|
|
289
|
+
if (!await this.verifyFileIntegrity(filePath)) {
|
|
290
|
+
this.logger.critical(`Integrity check failed for: ${filePath}`);
|
|
291
|
+
|
|
292
|
+
await this.quarantineFile(filePath, 'periodic_check_failed');
|
|
293
|
+
|
|
294
|
+
this.watchedFiles.delete(filePath);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async fileExists(filePath: string): Promise<boolean> {
|
|
300
|
+
try {
|
|
301
|
+
await fs.access(filePath);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async calculateFileHash(filePath: string): Promise<string> {
|
|
309
|
+
const content = await fs.readFile(filePath);
|
|
310
|
+
return crypto.createHash('sha512')
|
|
311
|
+
.update(content)
|
|
312
|
+
.digest('hex');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async getStoredHash(filePath: string): Promise<string | null> {
|
|
316
|
+
const hashFile = filePath + '.hash';
|
|
317
|
+
try {
|
|
318
|
+
return await fs.readFile(hashFile, 'utf8');
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private async findBackups(filePath: string): Promise<string[]> {
|
|
325
|
+
const backupDir = path.join(process.cwd(), '.backups');
|
|
326
|
+
const baseName = path.basename(filePath);
|
|
327
|
+
const pattern = new RegExp(`^${baseName}\\.[0-9]+\\.backup$`);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const files = await fs.readdir(backupDir);
|
|
331
|
+
return files
|
|
332
|
+
.filter(f => pattern.test(f))
|
|
333
|
+
.map(f => path.join(backupDir, f))
|
|
334
|
+
.sort()
|
|
335
|
+
.reverse();
|
|
336
|
+
} catch {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public getLockedFiles(): string[] {
|
|
342
|
+
return Array.from(this.fileLocks.entries())
|
|
343
|
+
.filter(([_, lock]) => lock.pid === process.pid)
|
|
344
|
+
.map(([file]) => file);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public getWatchedFiles(): string[] {
|
|
348
|
+
return Array.from(this.watchedFiles);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
public async getQuarantinedFiles(): Promise<string[]> {
|
|
352
|
+
try {
|
|
353
|
+
const files = await fs.readdir(this.quarantineDir);
|
|
354
|
+
return files.filter(f => !f.endsWith('.meta'));
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public deactivate(): void {
|
|
361
|
+
this.isActive = false;
|
|
362
|
+
|
|
363
|
+
for (const [filePath, lock] of this.fileLocks) {
|
|
364
|
+
if (lock.pid === process.pid) {
|
|
365
|
+
this.unlockFile(filePath);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const watchers = (global as any).fileWatchers;
|
|
370
|
+
if (watchers) {
|
|
371
|
+
for (const [_, watcher] of watchers) {
|
|
372
|
+
watcher.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public getStatus(): any {
|
|
378
|
+
return {
|
|
379
|
+
isActive: this.isActive,
|
|
380
|
+
lockedFiles: this.fileLocks.size,
|
|
381
|
+
watchedFiles: this.watchedFiles.size,
|
|
382
|
+
quarantineDir: this.quarantineDir
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|