@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
package/jest.config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
|
4
|
+
testMatch: [
|
|
5
|
+
'**/__tests__/**/*.+(ts|tsx|js)',
|
|
6
|
+
'**/?(*.)+(spec|test).+(ts|tsx|js)'
|
|
7
|
+
],
|
|
8
|
+
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.(ts|tsx)$': 'ts-jest'
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
14
|
+
collectCoverage: true,
|
|
15
|
+
coverageDirectory: 'coverage',
|
|
16
|
+
coverageReporters: ['text', 'lcov', 'clover', 'html'],
|
|
17
|
+
collectCoverageFrom: [
|
|
18
|
+
'src/**/*.{ts,tsx}',
|
|
19
|
+
'!src/**/*.d.ts',
|
|
20
|
+
'!src/types/**/*',
|
|
21
|
+
'!src/**/index.ts'
|
|
22
|
+
],
|
|
23
|
+
coverageThreshold: {
|
|
24
|
+
global: {
|
|
25
|
+
branches: 80,
|
|
26
|
+
functions: 80,
|
|
27
|
+
lines: 80,
|
|
28
|
+
statements: 80
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
|
33
|
+
moduleNameMapper: {
|
|
34
|
+
'^@/(.*)$': '<rootDir>/src/$1'
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
globals: {
|
|
38
|
+
'ts-jest': {
|
|
39
|
+
tsconfig: 'tsconfig.json',
|
|
40
|
+
diagnostics: true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
verbose: true,
|
|
45
|
+
testTimeout: 30000,
|
|
46
|
+
forceExit: true,
|
|
47
|
+
detectOpenHandles: true,
|
|
48
|
+
maxWorkers: process.env.CI ? 2 : '50%',
|
|
49
|
+
testSequencer: '@jest/test-sequencer',
|
|
50
|
+
watchPlugins: [
|
|
51
|
+
'jest-watch-typeahead/filename',
|
|
52
|
+
'jest-watch-typeahead/testname'
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
projects: [
|
|
56
|
+
{
|
|
57
|
+
displayName: 'unit',
|
|
58
|
+
testMatch: ['<rootDir>/tests/unit/**/*.test.ts']
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
displayName: 'integration',
|
|
62
|
+
testMatch: ['<rootDir>/tests/integration/**/*.test.ts']
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
displayName: 'security',
|
|
66
|
+
testMatch: ['<rootDir>/tests/security/**/*.test.ts']
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dimzxzzx07/file-watcher",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Enterprise-grade secure file watcher with military-grade integrity checking",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "ts-node src/index.ts",
|
|
11
|
+
"test": "jest",
|
|
12
|
+
"test:security": "jest tests/security",
|
|
13
|
+
"lint": "eslint src/**/*.ts",
|
|
14
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
15
|
+
"prepublishOnly": "npm run build && npm test",
|
|
16
|
+
"audit": "npm audit --production",
|
|
17
|
+
"scan": "snyk test"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"bcryptjs": "^2.4.3",
|
|
21
|
+
"chokidar": "^3.5.3",
|
|
22
|
+
"crypto-js": "^4.1.1",
|
|
23
|
+
"dotenv": "^16.3.1",
|
|
24
|
+
"pidusage": "^3.0.2",
|
|
25
|
+
"rate-limiter-flexible": "^2.4.1",
|
|
26
|
+
"systeminformation": "^5.21.7",
|
|
27
|
+
"winston": "^3.11.0",
|
|
28
|
+
"winston-daily-rotate-file": "^4.7.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/bcrypt": "^5.0.1",
|
|
32
|
+
"@types/chokidar": "^1.7.5",
|
|
33
|
+
"@types/crypto-js": "^4.1.1",
|
|
34
|
+
"@types/jest": "^29.5.5",
|
|
35
|
+
"@types/node": "^20.8.0",
|
|
36
|
+
"@types/node-forge": "^1.3.6",
|
|
37
|
+
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
|
38
|
+
"@typescript-eslint/parser": "^6.7.5",
|
|
39
|
+
"eslint": "^8.51.0",
|
|
40
|
+
"jest": "^29.7.0",
|
|
41
|
+
"jest-watch-typeahead": "^2.2.2",
|
|
42
|
+
"prettier": "^3.0.3",
|
|
43
|
+
"snyk": "^1.1230.0",
|
|
44
|
+
"ts-jest": "^29.1.1",
|
|
45
|
+
"ts-node": "^10.9.1",
|
|
46
|
+
"typescript": "^5.2.2"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0",
|
|
50
|
+
"npm": ">=9.0.0"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"security",
|
|
54
|
+
"file-watcher",
|
|
55
|
+
"integrity",
|
|
56
|
+
"enterprise",
|
|
57
|
+
"real-time",
|
|
58
|
+
"protection",
|
|
59
|
+
"anti-tamper",
|
|
60
|
+
"cryptographic",
|
|
61
|
+
"audit",
|
|
62
|
+
"compliance",
|
|
63
|
+
"zero-trust",
|
|
64
|
+
"military-grade"
|
|
65
|
+
],
|
|
66
|
+
"author": "Secure Enterprise Team",
|
|
67
|
+
"license": "UNLICENSED",
|
|
68
|
+
"private": false
|
|
69
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import { SecurityConfig, FileMetadata, BackupEntry } from '../types';
|
|
5
|
+
import { Logger } from '../utils/Logger';
|
|
6
|
+
|
|
7
|
+
export class BackupManager {
|
|
8
|
+
private readonly config: SecurityConfig;
|
|
9
|
+
private readonly logger: Logger;
|
|
10
|
+
private readonly backups: Map<string, BackupEntry[]>;
|
|
11
|
+
private readonly maxBackupsPerFile: number = 10;
|
|
12
|
+
|
|
13
|
+
constructor(config: SecurityConfig) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.logger = Logger.getInstance();
|
|
16
|
+
this.backups = new Map();
|
|
17
|
+
|
|
18
|
+
this.initializeBackupDirectory();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async initializeBackupDirectory(): Promise<void> {
|
|
22
|
+
try {
|
|
23
|
+
await fs.mkdir(this.config.backupDir, { recursive: true, mode: 0o700 });
|
|
24
|
+
await this.loadExistingBackups();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
this.logger.error('Failed to initialize backup directory', { error });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async loadExistingBackups(): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const files = await fs.readdir(this.config.backupDir);
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (file.endsWith('.backup')) {
|
|
36
|
+
const backupPath = path.join(this.config.backupDir, file);
|
|
37
|
+
const backupData = await this.readBackupFile(backupPath);
|
|
38
|
+
|
|
39
|
+
if (backupData) {
|
|
40
|
+
const backups = this.backups.get(backupData.originalPath) || [];
|
|
41
|
+
backups.push(backupData);
|
|
42
|
+
this.backups.set(backupData.originalPath, backups);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.logger.info(`Loaded ${this.backups.size} backup entries`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.error('Failed to load existing backups', { error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async readBackupFile(backupPath: string): Promise<BackupEntry | null> {
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(backupPath, 'utf8');
|
|
56
|
+
return JSON.parse(content) as BackupEntry;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async createBackup(filePath: string, metadata: FileMetadata): Promise<string> {
|
|
63
|
+
try {
|
|
64
|
+
const backupId = crypto.randomUUID();
|
|
65
|
+
const backupFileName = `${path.basename(filePath)}.${backupId}.backup`;
|
|
66
|
+
const backupPath = path.join(this.config.backupDir, backupFileName);
|
|
67
|
+
|
|
68
|
+
const content = await fs.readFile(filePath);
|
|
69
|
+
|
|
70
|
+
const backupEntry: BackupEntry = {
|
|
71
|
+
id: backupId,
|
|
72
|
+
originalPath: filePath,
|
|
73
|
+
backupPath,
|
|
74
|
+
timestamp: new Date(),
|
|
75
|
+
hash: metadata.hash,
|
|
76
|
+
encrypted: this.config.signatureVerification,
|
|
77
|
+
size: metadata.size,
|
|
78
|
+
metadata,
|
|
79
|
+
version: this.getNextVersion(filePath)
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let backupContent: Buffer | Uint8Array = content;
|
|
83
|
+
if (this.config.signatureVerification) {
|
|
84
|
+
backupContent = Buffer.from(await this.encryptBackup(content));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await fs.writeFile(backupPath, backupContent);
|
|
88
|
+
|
|
89
|
+
const metadataPath = backupPath + '.meta';
|
|
90
|
+
await fs.writeFile(metadataPath, JSON.stringify(backupEntry, null, 2));
|
|
91
|
+
|
|
92
|
+
const backups = this.backups.get(filePath) || [];
|
|
93
|
+
backups.push(backupEntry);
|
|
94
|
+
|
|
95
|
+
backups.sort((a, b) => b.version - a.version);
|
|
96
|
+
while (backups.length > this.maxBackupsPerFile) {
|
|
97
|
+
const oldBackup = backups.pop();
|
|
98
|
+
if (oldBackup) {
|
|
99
|
+
await this.deleteBackup(oldBackup);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.backups.set(filePath, backups);
|
|
104
|
+
|
|
105
|
+
this.logger.debug(`Backup created: ${filePath} -> ${backupPath}`);
|
|
106
|
+
return backupPath;
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logger.error(`Failed to create backup for ${filePath}`, { error });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public async rollbackFile(filePath: string, version?: number): Promise<boolean> {
|
|
115
|
+
try {
|
|
116
|
+
const backups = this.backups.get(filePath);
|
|
117
|
+
if (!backups || backups.length === 0) {
|
|
118
|
+
this.logger.warning(`No backups found for ${filePath}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let targetBackup: BackupEntry | undefined;
|
|
123
|
+
|
|
124
|
+
if (version) {
|
|
125
|
+
targetBackup = backups.find(b => b.version === version);
|
|
126
|
+
} else {
|
|
127
|
+
targetBackup = backups[0];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!targetBackup) {
|
|
131
|
+
this.logger.warning(`Backup version ${version} not found for ${filePath}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!await this.verifyBackup(targetBackup)) {
|
|
136
|
+
this.logger.error(`Backup integrity check failed for ${targetBackup.id}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let backupContent = await fs.readFile(targetBackup.backupPath);
|
|
141
|
+
|
|
142
|
+
if (targetBackup.encrypted) {
|
|
143
|
+
backupContent = Buffer.from(await this.decryptBackup(backupContent));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const hash = crypto.createHash(this.config.hashAlgorithm)
|
|
147
|
+
.update(backupContent)
|
|
148
|
+
.digest('hex');
|
|
149
|
+
|
|
150
|
+
if (hash !== targetBackup.hash) {
|
|
151
|
+
this.logger.error('Backup hash mismatch');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (await this.fileExists(filePath)) {
|
|
156
|
+
const currentContent = await fs.readFile(filePath);
|
|
157
|
+
const tempBackup = path.join(this.config.backupDir, 'pre_rollback_' + path.basename(filePath));
|
|
158
|
+
await fs.writeFile(tempBackup, currentContent);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await fs.writeFile(filePath, backupContent);
|
|
162
|
+
|
|
163
|
+
this.logger.info(`File rolled back: ${filePath} (version ${targetBackup.version})`);
|
|
164
|
+
return true;
|
|
165
|
+
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logger.error(`Failed to rollback ${filePath}`, { error });
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public async updateBackup(filePath: string, metadata: FileMetadata): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
const backups = this.backups.get(filePath);
|
|
175
|
+
if (!backups || backups.length === 0) {
|
|
176
|
+
await this.createBackup(filePath, metadata);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (backups[0] && backups[0].hash === metadata.hash) {
|
|
181
|
+
backups[0].timestamp = new Date();
|
|
182
|
+
|
|
183
|
+
const metadataPath = backups[0].backupPath + '.meta';
|
|
184
|
+
await fs.writeFile(metadataPath, JSON.stringify(backups[0], null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
await this.createBackup(filePath, metadata);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error(`Failed to update backup for ${filePath}`, { error });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public async restoreFile(filePath: string): Promise<boolean> {
|
|
195
|
+
return this.rollbackFile(filePath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public async performFinalBackup(): Promise<void> {
|
|
199
|
+
this.logger.info('Performing final backup...');
|
|
200
|
+
|
|
201
|
+
const entries = Array.from(this.backups.entries());
|
|
202
|
+
|
|
203
|
+
for (const [filePath, backups] of entries) {
|
|
204
|
+
if (backups.length > 0 && await this.fileExists(filePath)) {
|
|
205
|
+
const currentContent = await fs.readFile(filePath);
|
|
206
|
+
const finalBackupPath = path.join(
|
|
207
|
+
this.config.backupDir,
|
|
208
|
+
'final_' + path.basename(filePath)
|
|
209
|
+
);
|
|
210
|
+
await fs.writeFile(finalBackupPath, currentContent);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.logger.info('Final backup completed');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async verifyBackup(backup: BackupEntry): Promise<boolean> {
|
|
218
|
+
try {
|
|
219
|
+
if (!await this.fileExists(backup.backupPath)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const metadataPath = backup.backupPath + '.meta';
|
|
224
|
+
if (!await this.fileExists(metadataPath)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const stats = await fs.stat(backup.backupPath);
|
|
229
|
+
if (stats.size !== backup.size) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return true;
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async deleteBackup(backup: BackupEntry): Promise<void> {
|
|
240
|
+
try {
|
|
241
|
+
await fs.unlink(backup.backupPath);
|
|
242
|
+
|
|
243
|
+
const metadataPath = backup.backupPath + '.meta';
|
|
244
|
+
try {
|
|
245
|
+
await fs.unlink(metadataPath);
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.logger.debug(`Deleted old backup: ${backup.id}`);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.logger.error(`Failed to delete backup ${backup.id}`, { error });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async encryptBackup(data: Buffer): Promise<Uint8Array> {
|
|
256
|
+
const key = crypto.randomBytes(32);
|
|
257
|
+
const iv = crypto.randomBytes(16);
|
|
258
|
+
|
|
259
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
260
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
261
|
+
|
|
262
|
+
return Buffer.concat([iv, encrypted]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async decryptBackup(data: Buffer): Promise<Uint8Array> {
|
|
266
|
+
const iv = data.subarray(0, 16);
|
|
267
|
+
const encrypted = data.subarray(16);
|
|
268
|
+
|
|
269
|
+
const key = crypto.randomBytes(32);
|
|
270
|
+
|
|
271
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
272
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private getNextVersion(filePath: string): number {
|
|
276
|
+
const backups = this.backups.get(filePath) || [];
|
|
277
|
+
if (backups.length === 0) return 1;
|
|
278
|
+
|
|
279
|
+
const maxVersion = Math.max(...backups.map(b => b.version));
|
|
280
|
+
return maxVersion + 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async fileExists(filePath: string): Promise<boolean> {
|
|
284
|
+
try {
|
|
285
|
+
await fs.access(filePath);
|
|
286
|
+
return true;
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
public getBackups(filePath: string): BackupEntry[] {
|
|
293
|
+
return this.backups.get(filePath) || [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public getStatus(): any {
|
|
297
|
+
return {
|
|
298
|
+
totalBackups: Array.from(this.backups.values()).reduce(
|
|
299
|
+
(sum, backups) => sum + backups.length, 0
|
|
300
|
+
),
|
|
301
|
+
filesWithBackups: this.backups.size,
|
|
302
|
+
backupDir: this.config.backupDir
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { SecurityConfig, FileMetadata } from '../types';
|
|
5
|
+
import { HashGenerator } from '../crypto/HashGenerator';
|
|
6
|
+
import { SignatureValidator } from '../crypto/SignatureValidator';
|
|
7
|
+
import { InjectionDetector } from '../detectors/InjectionDetector';
|
|
8
|
+
import { Logger } from '../utils/Logger';
|
|
9
|
+
|
|
10
|
+
export class IntegrityValidator {
|
|
11
|
+
private readonly hashGenerator: HashGenerator;
|
|
12
|
+
private readonly signatureValidator: SignatureValidator;
|
|
13
|
+
private readonly injectionDetector: InjectionDetector;
|
|
14
|
+
private readonly logger: Logger;
|
|
15
|
+
private readonly config: SecurityConfig;
|
|
16
|
+
|
|
17
|
+
constructor(config: SecurityConfig) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.logger = Logger.getInstance();
|
|
20
|
+
this.hashGenerator = new HashGenerator(config);
|
|
21
|
+
this.signatureValidator = new SignatureValidator();
|
|
22
|
+
this.injectionDetector = new InjectionDetector();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async generateFileMetadata(filePath: string): Promise<FileMetadata> {
|
|
26
|
+
try {
|
|
27
|
+
const stats = await fs.stat(filePath);
|
|
28
|
+
const content = await fs.readFile(filePath);
|
|
29
|
+
const hash = this.hashGenerator.generateHash(content);
|
|
30
|
+
const encryptedHash = this.hashGenerator.generateEncryptedHash(content);
|
|
31
|
+
const entropy = this.calculateEntropy(content);
|
|
32
|
+
const magicBytes = await this.getMagicBytes(filePath);
|
|
33
|
+
|
|
34
|
+
let signature: string | undefined;
|
|
35
|
+
if (this.config.signatureVerification) {
|
|
36
|
+
signature = await this.signatureValidator.generateSignature(content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
path: filePath,
|
|
41
|
+
hash,
|
|
42
|
+
signature,
|
|
43
|
+
size: stats.size,
|
|
44
|
+
created: stats.birthtime,
|
|
45
|
+
modified: stats.mtime,
|
|
46
|
+
accessed: stats.atime,
|
|
47
|
+
permissions: stats.mode,
|
|
48
|
+
owner: stats.uid.toString(),
|
|
49
|
+
group: stats.gid.toString(),
|
|
50
|
+
inode: stats.ino,
|
|
51
|
+
checksum: this.generateChecksum(content),
|
|
52
|
+
version: 1,
|
|
53
|
+
encryptedHash,
|
|
54
|
+
entropy,
|
|
55
|
+
magicBytes
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.logger.error(`Failed to generate metadata for ${filePath}`, { error });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async validateFile(filePath: string, metadata: FileMetadata): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
if (!await this.fileExists(filePath)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stats = await fs.stat(filePath);
|
|
70
|
+
const content = await fs.readFile(filePath);
|
|
71
|
+
const currentHash = this.hashGenerator.generateHash(content);
|
|
72
|
+
|
|
73
|
+
if (currentHash !== metadata.hash) {
|
|
74
|
+
this.logger.warning(`Hash mismatch for ${filePath}`, {
|
|
75
|
+
expected: metadata.hash,
|
|
76
|
+
actual: currentHash
|
|
77
|
+
});
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (stats.size !== metadata.size) {
|
|
82
|
+
this.logger.warning(`Size mismatch for ${filePath}`, {
|
|
83
|
+
expected: metadata.size,
|
|
84
|
+
actual: stats.size
|
|
85
|
+
});
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (stats.ino !== metadata.inode) {
|
|
90
|
+
this.logger.warning(`Inode mismatch for ${filePath}`, {
|
|
91
|
+
expected: metadata.inode,
|
|
92
|
+
actual: stats.ino
|
|
93
|
+
});
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.isTextFile(filePath)) {
|
|
98
|
+
const textContent = content.toString('utf8');
|
|
99
|
+
if (await this.injectionDetector.detectInjection(textContent)) {
|
|
100
|
+
this.logger.warning(`Injection detected in ${filePath}`);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.config.signatureVerification && metadata.signature) {
|
|
106
|
+
const isValidSignature = await this.signatureValidator.validateSignature(
|
|
107
|
+
content,
|
|
108
|
+
metadata.signature
|
|
109
|
+
);
|
|
110
|
+
if (!isValidSignature) {
|
|
111
|
+
this.logger.warning(`Invalid signature for ${filePath}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.config.integrityLevel === 'paranoid') {
|
|
117
|
+
const currentEntropy = this.calculateEntropy(content);
|
|
118
|
+
if (Math.abs(currentEntropy - metadata.entropy) > 0.1) {
|
|
119
|
+
this.logger.warning(`Entropy changed for ${filePath}`);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const currentMagic = await this.getMagicBytes(filePath);
|
|
124
|
+
if (currentMagic !== metadata.magicBytes) {
|
|
125
|
+
this.logger.warning(`Magic bytes changed for ${filePath}`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.logger.error(`Validation error for ${filePath}`, { error });
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public async verifySignature(filePath: string, metadata: FileMetadata): Promise<boolean> {
|
|
138
|
+
if (!metadata.signature) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const content = await fs.readFile(filePath);
|
|
144
|
+
return await this.signatureValidator.validateSignature(content, metadata.signature);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.logger.error(`Signature verification failed for ${filePath}`, { error });
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private calculateEntropy(data: Buffer): number {
|
|
152
|
+
const frequencies: { [key: number]: number } = {};
|
|
153
|
+
|
|
154
|
+
for (const byte of data) {
|
|
155
|
+
frequencies[byte] = (frequencies[byte] || 0) + 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let entropy = 0;
|
|
159
|
+
const length = data.length;
|
|
160
|
+
|
|
161
|
+
for (const freq of Object.values(frequencies)) {
|
|
162
|
+
const probability = freq / length;
|
|
163
|
+
entropy -= probability * Math.log2(probability);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return entropy;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async getMagicBytes(filePath: string): Promise<string | undefined> {
|
|
170
|
+
try {
|
|
171
|
+
const buffer = Buffer.alloc(8);
|
|
172
|
+
const fd = await fs.open(filePath, 'r');
|
|
173
|
+
await fd.read(buffer, 0, 8, 0);
|
|
174
|
+
await fd.close();
|
|
175
|
+
|
|
176
|
+
return buffer.toString('hex').substring(0, 8);
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private generateChecksum(data: Buffer): string {
|
|
183
|
+
return crypto.createHash('md5').update(data).digest('hex');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async fileExists(filePath: string): Promise<boolean> {
|
|
187
|
+
try {
|
|
188
|
+
await fs.access(filePath);
|
|
189
|
+
return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private isTextFile(filePath: string): boolean {
|
|
196
|
+
const textExtensions = ['.js', '.ts', '.json', '.txt', '.md', '.html', '.css', '.xml'];
|
|
197
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
198
|
+
return textExtensions.includes(ext);
|
|
199
|
+
}
|
|
200
|
+
}
|