filemayor 3.6.0 → 4.0.7
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/LICENSE +90 -90
- package/README.md +96 -96
- package/core/analyzer.js +163 -163
- package/core/categories.js +235 -235
- package/core/cleaner.js +527 -527
- package/core/config.js +562 -562
- package/core/fs-abstraction.js +110 -38
- package/core/index.js +135 -135
- package/core/intent-interpreter.js +228 -66
- package/core/license.js +403 -339
- package/core/organizer.js +414 -414
- package/core/reporter.js +783 -783
- package/core/scanner.js +466 -466
- package/core/security.js +370 -370
- package/core/sop-parser.js +564 -564
- package/core/telemetry.js +74 -0
- package/core/vault.js +83 -69
- package/core/watcher.js +478 -478
- package/index.js +895 -877
- package/package.json +3 -3
package/core/security.js
CHANGED
|
@@ -1,370 +1,370 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
-
* FILEMAYOR CORE — SECURITY
|
|
6
|
-
* Path traversal guards, input sanitization, and safety checks
|
|
7
|
-
* ═══════════════════════════════════════════════════════════════════
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const os = require('os');
|
|
15
|
-
|
|
16
|
-
// ─── Dangerous Paths ──────────────────────────────────────────────
|
|
17
|
-
const PROTECTED_DIRECTORIES = new Set([
|
|
18
|
-
// System roots
|
|
19
|
-
'/', 'C:\\', 'C:\\Windows', 'C:\\Windows\\System32',
|
|
20
|
-
'/System', '/Library', '/usr', '/bin', '/sbin', '/etc',
|
|
21
|
-
'/boot', '/dev', '/proc', '/sys', '/run', '/var',
|
|
22
|
-
|
|
23
|
-
// User critical
|
|
24
|
-
path.join(os.homedir()), // Don't organize the entire home dir
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
const PROTECTED_PREFIXES = [
|
|
28
|
-
'/System', '/Library/System',
|
|
29
|
-
'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
|
|
30
|
-
'C:\\ProgramData', 'C:\\Users\\Public',
|
|
31
|
-
'/usr/lib', '/usr/bin', '/usr/sbin',
|
|
32
|
-
'/var/lib', '/var/run', '/boot', '/dev', '/proc', '/sys'
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const DANGEROUS_EXTENSIONS = new Set([
|
|
36
|
-
'.sys', '.drv', '.dll', '.so', '.dylib', '.kext',
|
|
37
|
-
'.plist', '.reg', '.msc', '.cpl'
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
// ─── Project Substrates (v3.5) ────────────────────────────────────
|
|
41
|
-
const SUBSTRATE_MARKERS = [
|
|
42
|
-
'.git', '.svn', 'node_modules', 'package.json',
|
|
43
|
-
'venv', '.venv', '__pycache__', 'env',
|
|
44
|
-
'pom.xml', 'build.gradle', 'CMakeLists.txt',
|
|
45
|
-
'Makefile', '.hg', 'composer.json', 'go.mod'
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
// ─── Path Validation ──────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Resolve and normalize a path, preventing traversal attacks
|
|
52
|
-
* @param {string} inputPath - Raw path from user input
|
|
53
|
-
* @param {string} [basePath] - Optional base to restrict resolution within
|
|
54
|
-
* @returns {{ valid: boolean, resolved: string, error?: string }}
|
|
55
|
-
*/
|
|
56
|
-
function validatePath(inputPath, basePath = null) {
|
|
57
|
-
if (!inputPath || typeof inputPath !== 'string') {
|
|
58
|
-
return { valid: false, resolved: '', error: 'Path is empty or invalid' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// [Hacker-Proof] Reject null bytes (classic path traversal vector)
|
|
62
|
-
if (inputPath.includes('\0')) {
|
|
63
|
-
return { valid: false, resolved: '', error: 'Path contains null bytes (rejected)' };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// [Hacker-Proof] Resolve to absolute and normalize to handle '..' etc.
|
|
67
|
-
const resolved = path.resolve(inputPath);
|
|
68
|
-
const rLower = resolved.toLowerCase();
|
|
69
|
-
|
|
70
|
-
// [Hacker-Proof] Jail Logic: If a basePath is given, ensure resolved path stays within it
|
|
71
|
-
if (basePath) {
|
|
72
|
-
const resolvedBase = path.resolve(basePath).toLowerCase();
|
|
73
|
-
|
|
74
|
-
// [Elite Fix] Robust jailing: Target must start with base + separator or be the base itself
|
|
75
|
-
const isSafe = rLower === resolvedBase || rLower.startsWith(resolvedBase + path.sep);
|
|
76
|
-
|
|
77
|
-
if (!isSafe) {
|
|
78
|
-
return {
|
|
79
|
-
valid: false,
|
|
80
|
-
resolved,
|
|
81
|
-
error: `Security Alert: Path Traversal Attempted. "${resolved}" is outside scope.`
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// [Hacker-Proof] Check against protected directories
|
|
87
|
-
if (PROTECTED_DIRECTORIES.has(resolved)) {
|
|
88
|
-
return {
|
|
89
|
-
valid: false,
|
|
90
|
-
resolved,
|
|
91
|
-
error: `Refusing to operate on protected directory: "${resolved}"`
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// [Hacker-Proof] Check against protected prefixes (System/Library/Program Files)
|
|
96
|
-
for (const prefix of PROTECTED_PREFIXES) {
|
|
97
|
-
const normalizedPrefix = path.resolve(prefix).toLowerCase();
|
|
98
|
-
const rLower = resolved.toLowerCase();
|
|
99
|
-
|
|
100
|
-
// Block if exact match OR if it starts with prefix + separator
|
|
101
|
-
if (rLower === normalizedPrefix || rLower.startsWith(normalizedPrefix + path.sep)) {
|
|
102
|
-
return {
|
|
103
|
-
valid: false,
|
|
104
|
-
resolved,
|
|
105
|
-
error: `Refusal: Path is within a system-protected root ("${resolved}")`
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { valid: true, resolved };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Check if a file is safe to move/delete (not a system critical file)
|
|
115
|
-
* @param {string} filePath - Absolute file path
|
|
116
|
-
* @returns {{ safe: boolean, reason?: string }}
|
|
117
|
-
*/
|
|
118
|
-
function isFileSafe(filePath) {
|
|
119
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
120
|
-
return { safe: false, reason: 'File path is empty or invalid' };
|
|
121
|
-
}
|
|
122
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
123
|
-
const basename = path.basename(filePath);
|
|
124
|
-
|
|
125
|
-
// Reject system-critical extensions
|
|
126
|
-
if (DANGEROUS_EXTENSIONS.has(ext)) {
|
|
127
|
-
return { safe: false, reason: `System file extension "${ext}" is protected` };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Reject Windows system files
|
|
131
|
-
const systemFiles = ['ntldr', 'bootmgr', 'pagefile.sys', 'swapfile.sys', 'hiberfil.sys'];
|
|
132
|
-
if (systemFiles.includes(basename.toLowerCase())) {
|
|
133
|
-
return { safe: false, reason: `System file "${basename}" is protected` };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Reject Unix critical files
|
|
137
|
-
const unixCritical = ['.bashrc', '.zshrc', '.profile', '.bash_profile', '.ssh'];
|
|
138
|
-
if (unixCritical.includes(basename.toLowerCase())) {
|
|
139
|
-
return { safe: false, reason: `User config file "${basename}" is protected` };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return { safe: true };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Check if a directory is a "Project Substrate" (should not be scattered)
|
|
147
|
-
* @param {string} dirPath - Directory to check
|
|
148
|
-
* @returns {boolean}
|
|
149
|
-
*/
|
|
150
|
-
function isSubstrateCritical(dirPath) {
|
|
151
|
-
try {
|
|
152
|
-
const items = fs.readdirSync(dirPath);
|
|
153
|
-
return SUBSTRATE_MARKERS.some(marker => items.includes(marker));
|
|
154
|
-
} catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Find the nearest substrate parent for a file
|
|
161
|
-
* @param {string} filePath
|
|
162
|
-
* @returns {string|null}
|
|
163
|
-
*/
|
|
164
|
-
function findNearestSubstrate(filePath) {
|
|
165
|
-
let current = path.dirname(path.resolve(filePath));
|
|
166
|
-
const root = path.parse(current).root;
|
|
167
|
-
|
|
168
|
-
while (current !== root) {
|
|
169
|
-
if (isSubstrateCritical(current)) return current;
|
|
170
|
-
current = path.dirname(current);
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Check if a directory is safe to scan/organize
|
|
178
|
-
* @param {string} dirPath - Absolute directory path
|
|
179
|
-
* @returns {{ safe: boolean, reason?: string }}
|
|
180
|
-
*/
|
|
181
|
-
function isDirSafe(dirPath) {
|
|
182
|
-
const validation = validatePath(dirPath);
|
|
183
|
-
if (!validation.valid) {
|
|
184
|
-
return { safe: false, reason: validation.error };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Don't operate on root of any drive
|
|
188
|
-
const parsed = path.parse(validation.resolved);
|
|
189
|
-
if (parsed.dir === parsed.root && parsed.base === '') {
|
|
190
|
-
return { safe: false, reason: 'Cannot operate on filesystem root' };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return { safe: true };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ─── Input Sanitization ───────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Sanitize a filename for safe filesystem operations
|
|
200
|
-
* @param {string} filename - Raw filename
|
|
201
|
-
* @returns {string} Sanitized filename
|
|
202
|
-
*/
|
|
203
|
-
function sanitizeFilename(filename) {
|
|
204
|
-
if (!filename || typeof filename !== 'string') return 'unnamed';
|
|
205
|
-
|
|
206
|
-
return filename
|
|
207
|
-
// Remove null bytes
|
|
208
|
-
.replace(/\0/g, '')
|
|
209
|
-
// Remove path separators
|
|
210
|
-
.replace(/[/\\]/g, '_')
|
|
211
|
-
// Remove other dangerous characters
|
|
212
|
-
.replace(/[<>:"|?*]/g, '_')
|
|
213
|
-
// Remove control characters
|
|
214
|
-
.replace(/[\x00-\x1f\x80-\x9f]/g, '')
|
|
215
|
-
// Collapse multiple underscores/spaces
|
|
216
|
-
.replace(/_{2,}/g, '_')
|
|
217
|
-
.replace(/\s{2,}/g, ' ')
|
|
218
|
-
// Remove leading/trailing dots and spaces (Windows issue)
|
|
219
|
-
.replace(/^[\s.]+|[\s.]+$/g, '')
|
|
220
|
-
// Ensure not empty after sanitization
|
|
221
|
-
|| 'unnamed';
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Sanitize a directory name
|
|
226
|
-
* @param {string} dirname - Raw directory name
|
|
227
|
-
* @returns {string} Sanitized directory name
|
|
228
|
-
*/
|
|
229
|
-
function sanitizeDirname(dirname) {
|
|
230
|
-
return sanitizeFilename(dirname);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ─── Permission Checks ───────────────────────────────────────────
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Check if we have read access to a path
|
|
237
|
-
* @param {string} targetPath - Path to check
|
|
238
|
-
* @returns {boolean} Whether we have read access
|
|
239
|
-
*/
|
|
240
|
-
function canRead(targetPath) {
|
|
241
|
-
try {
|
|
242
|
-
fs.accessSync(targetPath, fs.constants.R_OK);
|
|
243
|
-
return true;
|
|
244
|
-
} catch {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Check if we have write access to a path
|
|
251
|
-
* @param {string} targetPath - Path to check
|
|
252
|
-
* @returns {boolean} Whether we have write access
|
|
253
|
-
*/
|
|
254
|
-
function canWrite(targetPath) {
|
|
255
|
-
try {
|
|
256
|
-
fs.accessSync(targetPath, fs.constants.W_OK);
|
|
257
|
-
return true;
|
|
258
|
-
} catch {
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Check full operation permissions for a directory
|
|
265
|
-
* @param {string} dirPath - Directory to check
|
|
266
|
-
* @returns {{ read: boolean, write: boolean, execute: boolean }}
|
|
267
|
-
*/
|
|
268
|
-
function checkPermissions(dirPath) {
|
|
269
|
-
const result = { read: false, write: false, execute: false };
|
|
270
|
-
try { fs.accessSync(dirPath, fs.constants.R_OK); result.read = true; } catch { /* */ }
|
|
271
|
-
try { fs.accessSync(dirPath, fs.constants.W_OK); result.write = true; } catch { /* */ }
|
|
272
|
-
try { fs.accessSync(dirPath, fs.constants.X_OK); result.execute = true; } catch { /* */ }
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ─── Rate Limiting (for CLI/server use) ───────────────────────────
|
|
277
|
-
|
|
278
|
-
class OperationThrottle {
|
|
279
|
-
constructor(maxOps = 1000, windowMs = 60000) {
|
|
280
|
-
this.maxOps = maxOps;
|
|
281
|
-
this.windowMs = windowMs;
|
|
282
|
-
this.operations = [];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
canProceed() {
|
|
286
|
-
const now = Date.now();
|
|
287
|
-
this.operations = this.operations.filter(t => now - t < this.windowMs);
|
|
288
|
-
if (this.operations.length >= this.maxOps) {
|
|
289
|
-
return {
|
|
290
|
-
allowed: false,
|
|
291
|
-
retryAfterMs: this.windowMs - (now - this.operations[0]),
|
|
292
|
-
reason: `Rate limit: max ${this.maxOps} operations per ${this.windowMs / 1000}s`
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
this.operations.push(now);
|
|
296
|
-
return { allowed: true };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
reset() {
|
|
300
|
-
this.operations = [];
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ─── Integrity Verification ───────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Create a pre-operation snapshot for rollback verification
|
|
308
|
-
* @param {string} filePath - File to snapshot
|
|
309
|
-
* @returns {{ exists: boolean, size?: number, modified?: Date, path: string }}
|
|
310
|
-
*/
|
|
311
|
-
function createSnapshot(filePath) {
|
|
312
|
-
try {
|
|
313
|
-
const stat = fs.statSync(filePath);
|
|
314
|
-
return {
|
|
315
|
-
exists: true,
|
|
316
|
-
size: stat.size,
|
|
317
|
-
modified: stat.mtime,
|
|
318
|
-
path: path.resolve(filePath)
|
|
319
|
-
};
|
|
320
|
-
} catch {
|
|
321
|
-
return { exists: false, path: path.resolve(filePath) };
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Verify a file matches a previous snapshot
|
|
327
|
-
* @param {string} filePath - File to verify
|
|
328
|
-
* @param {Object} snapshot - Previous snapshot
|
|
329
|
-
* @returns {{ matches: boolean, differences: string[] }}
|
|
330
|
-
*/
|
|
331
|
-
function verifySnapshot(filePath, snapshot) {
|
|
332
|
-
const differences = [];
|
|
333
|
-
try {
|
|
334
|
-
const stat = fs.statSync(filePath);
|
|
335
|
-
if (!snapshot.exists) {
|
|
336
|
-
differences.push('File now exists but previously did not');
|
|
337
|
-
} else {
|
|
338
|
-
if (stat.size !== snapshot.size) {
|
|
339
|
-
differences.push(`Size changed: ${snapshot.size} → ${stat.size}`);
|
|
340
|
-
}
|
|
341
|
-
if (stat.mtime.getTime() !== snapshot.modified.getTime()) {
|
|
342
|
-
differences.push('Modification time changed');
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
} catch {
|
|
346
|
-
if (snapshot.exists) {
|
|
347
|
-
differences.push('File no longer exists');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return { matches: differences.length === 0, differences };
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
module.exports = {
|
|
354
|
-
validatePath,
|
|
355
|
-
isFileSafe,
|
|
356
|
-
isDirSafe,
|
|
357
|
-
isSubstrateCritical,
|
|
358
|
-
findNearestSubstrate,
|
|
359
|
-
sanitizeFilename,
|
|
360
|
-
sanitizeDirname,
|
|
361
|
-
canRead,
|
|
362
|
-
canWrite,
|
|
363
|
-
checkPermissions,
|
|
364
|
-
OperationThrottle,
|
|
365
|
-
createSnapshot,
|
|
366
|
-
verifySnapshot,
|
|
367
|
-
PROTECTED_DIRECTORIES,
|
|
368
|
-
PROTECTED_PREFIXES,
|
|
369
|
-
DANGEROUS_EXTENSIONS
|
|
370
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — SECURITY
|
|
6
|
+
* Path traversal guards, input sanitization, and safety checks
|
|
7
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
// ─── Dangerous Paths ──────────────────────────────────────────────
|
|
17
|
+
const PROTECTED_DIRECTORIES = new Set([
|
|
18
|
+
// System roots
|
|
19
|
+
'/', 'C:\\', 'C:\\Windows', 'C:\\Windows\\System32',
|
|
20
|
+
'/System', '/Library', '/usr', '/bin', '/sbin', '/etc',
|
|
21
|
+
'/boot', '/dev', '/proc', '/sys', '/run', '/var',
|
|
22
|
+
|
|
23
|
+
// User critical
|
|
24
|
+
path.join(os.homedir()), // Don't organize the entire home dir
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const PROTECTED_PREFIXES = [
|
|
28
|
+
'/System', '/Library/System',
|
|
29
|
+
'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)',
|
|
30
|
+
'C:\\ProgramData', 'C:\\Users\\Public',
|
|
31
|
+
'/usr/lib', '/usr/bin', '/usr/sbin',
|
|
32
|
+
'/var/lib', '/var/run', '/boot', '/dev', '/proc', '/sys'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const DANGEROUS_EXTENSIONS = new Set([
|
|
36
|
+
'.sys', '.drv', '.dll', '.so', '.dylib', '.kext',
|
|
37
|
+
'.plist', '.reg', '.msc', '.cpl'
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// ─── Project Substrates (v3.5) ────────────────────────────────────
|
|
41
|
+
const SUBSTRATE_MARKERS = [
|
|
42
|
+
'.git', '.svn', 'node_modules', 'package.json',
|
|
43
|
+
'venv', '.venv', '__pycache__', 'env',
|
|
44
|
+
'pom.xml', 'build.gradle', 'CMakeLists.txt',
|
|
45
|
+
'Makefile', '.hg', 'composer.json', 'go.mod'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// ─── Path Validation ──────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve and normalize a path, preventing traversal attacks
|
|
52
|
+
* @param {string} inputPath - Raw path from user input
|
|
53
|
+
* @param {string} [basePath] - Optional base to restrict resolution within
|
|
54
|
+
* @returns {{ valid: boolean, resolved: string, error?: string }}
|
|
55
|
+
*/
|
|
56
|
+
function validatePath(inputPath, basePath = null) {
|
|
57
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
58
|
+
return { valid: false, resolved: '', error: 'Path is empty or invalid' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// [Hacker-Proof] Reject null bytes (classic path traversal vector)
|
|
62
|
+
if (inputPath.includes('\0')) {
|
|
63
|
+
return { valid: false, resolved: '', error: 'Path contains null bytes (rejected)' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// [Hacker-Proof] Resolve to absolute and normalize to handle '..' etc.
|
|
67
|
+
const resolved = path.resolve(inputPath);
|
|
68
|
+
const rLower = resolved.toLowerCase();
|
|
69
|
+
|
|
70
|
+
// [Hacker-Proof] Jail Logic: If a basePath is given, ensure resolved path stays within it
|
|
71
|
+
if (basePath) {
|
|
72
|
+
const resolvedBase = path.resolve(basePath).toLowerCase();
|
|
73
|
+
|
|
74
|
+
// [Elite Fix] Robust jailing: Target must start with base + separator or be the base itself
|
|
75
|
+
const isSafe = rLower === resolvedBase || rLower.startsWith(resolvedBase + path.sep);
|
|
76
|
+
|
|
77
|
+
if (!isSafe) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
resolved,
|
|
81
|
+
error: `Security Alert: Path Traversal Attempted. "${resolved}" is outside scope.`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// [Hacker-Proof] Check against protected directories
|
|
87
|
+
if (PROTECTED_DIRECTORIES.has(resolved)) {
|
|
88
|
+
return {
|
|
89
|
+
valid: false,
|
|
90
|
+
resolved,
|
|
91
|
+
error: `Refusing to operate on protected directory: "${resolved}"`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// [Hacker-Proof] Check against protected prefixes (System/Library/Program Files)
|
|
96
|
+
for (const prefix of PROTECTED_PREFIXES) {
|
|
97
|
+
const normalizedPrefix = path.resolve(prefix).toLowerCase();
|
|
98
|
+
const rLower = resolved.toLowerCase();
|
|
99
|
+
|
|
100
|
+
// Block if exact match OR if it starts with prefix + separator
|
|
101
|
+
if (rLower === normalizedPrefix || rLower.startsWith(normalizedPrefix + path.sep)) {
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
resolved,
|
|
105
|
+
error: `Refusal: Path is within a system-protected root ("${resolved}")`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { valid: true, resolved };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a file is safe to move/delete (not a system critical file)
|
|
115
|
+
* @param {string} filePath - Absolute file path
|
|
116
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
117
|
+
*/
|
|
118
|
+
function isFileSafe(filePath) {
|
|
119
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
120
|
+
return { safe: false, reason: 'File path is empty or invalid' };
|
|
121
|
+
}
|
|
122
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
123
|
+
const basename = path.basename(filePath);
|
|
124
|
+
|
|
125
|
+
// Reject system-critical extensions
|
|
126
|
+
if (DANGEROUS_EXTENSIONS.has(ext)) {
|
|
127
|
+
return { safe: false, reason: `System file extension "${ext}" is protected` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Reject Windows system files
|
|
131
|
+
const systemFiles = ['ntldr', 'bootmgr', 'pagefile.sys', 'swapfile.sys', 'hiberfil.sys'];
|
|
132
|
+
if (systemFiles.includes(basename.toLowerCase())) {
|
|
133
|
+
return { safe: false, reason: `System file "${basename}" is protected` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reject Unix critical files
|
|
137
|
+
const unixCritical = ['.bashrc', '.zshrc', '.profile', '.bash_profile', '.ssh'];
|
|
138
|
+
if (unixCritical.includes(basename.toLowerCase())) {
|
|
139
|
+
return { safe: false, reason: `User config file "${basename}" is protected` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { safe: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a directory is a "Project Substrate" (should not be scattered)
|
|
147
|
+
* @param {string} dirPath - Directory to check
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
function isSubstrateCritical(dirPath) {
|
|
151
|
+
try {
|
|
152
|
+
const items = fs.readdirSync(dirPath);
|
|
153
|
+
return SUBSTRATE_MARKERS.some(marker => items.includes(marker));
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Find the nearest substrate parent for a file
|
|
161
|
+
* @param {string} filePath
|
|
162
|
+
* @returns {string|null}
|
|
163
|
+
*/
|
|
164
|
+
function findNearestSubstrate(filePath) {
|
|
165
|
+
let current = path.dirname(path.resolve(filePath));
|
|
166
|
+
const root = path.parse(current).root;
|
|
167
|
+
|
|
168
|
+
while (current !== root) {
|
|
169
|
+
if (isSubstrateCritical(current)) return current;
|
|
170
|
+
current = path.dirname(current);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if a directory is safe to scan/organize
|
|
178
|
+
* @param {string} dirPath - Absolute directory path
|
|
179
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
180
|
+
*/
|
|
181
|
+
function isDirSafe(dirPath) {
|
|
182
|
+
const validation = validatePath(dirPath);
|
|
183
|
+
if (!validation.valid) {
|
|
184
|
+
return { safe: false, reason: validation.error };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Don't operate on root of any drive
|
|
188
|
+
const parsed = path.parse(validation.resolved);
|
|
189
|
+
if (parsed.dir === parsed.root && parsed.base === '') {
|
|
190
|
+
return { safe: false, reason: 'Cannot operate on filesystem root' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { safe: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Input Sanitization ───────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sanitize a filename for safe filesystem operations
|
|
200
|
+
* @param {string} filename - Raw filename
|
|
201
|
+
* @returns {string} Sanitized filename
|
|
202
|
+
*/
|
|
203
|
+
function sanitizeFilename(filename) {
|
|
204
|
+
if (!filename || typeof filename !== 'string') return 'unnamed';
|
|
205
|
+
|
|
206
|
+
return filename
|
|
207
|
+
// Remove null bytes
|
|
208
|
+
.replace(/\0/g, '')
|
|
209
|
+
// Remove path separators
|
|
210
|
+
.replace(/[/\\]/g, '_')
|
|
211
|
+
// Remove other dangerous characters
|
|
212
|
+
.replace(/[<>:"|?*]/g, '_')
|
|
213
|
+
// Remove control characters
|
|
214
|
+
.replace(/[\x00-\x1f\x80-\x9f]/g, '')
|
|
215
|
+
// Collapse multiple underscores/spaces
|
|
216
|
+
.replace(/_{2,}/g, '_')
|
|
217
|
+
.replace(/\s{2,}/g, ' ')
|
|
218
|
+
// Remove leading/trailing dots and spaces (Windows issue)
|
|
219
|
+
.replace(/^[\s.]+|[\s.]+$/g, '')
|
|
220
|
+
// Ensure not empty after sanitization
|
|
221
|
+
|| 'unnamed';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Sanitize a directory name
|
|
226
|
+
* @param {string} dirname - Raw directory name
|
|
227
|
+
* @returns {string} Sanitized directory name
|
|
228
|
+
*/
|
|
229
|
+
function sanitizeDirname(dirname) {
|
|
230
|
+
return sanitizeFilename(dirname);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Permission Checks ───────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if we have read access to a path
|
|
237
|
+
* @param {string} targetPath - Path to check
|
|
238
|
+
* @returns {boolean} Whether we have read access
|
|
239
|
+
*/
|
|
240
|
+
function canRead(targetPath) {
|
|
241
|
+
try {
|
|
242
|
+
fs.accessSync(targetPath, fs.constants.R_OK);
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if we have write access to a path
|
|
251
|
+
* @param {string} targetPath - Path to check
|
|
252
|
+
* @returns {boolean} Whether we have write access
|
|
253
|
+
*/
|
|
254
|
+
function canWrite(targetPath) {
|
|
255
|
+
try {
|
|
256
|
+
fs.accessSync(targetPath, fs.constants.W_OK);
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check full operation permissions for a directory
|
|
265
|
+
* @param {string} dirPath - Directory to check
|
|
266
|
+
* @returns {{ read: boolean, write: boolean, execute: boolean }}
|
|
267
|
+
*/
|
|
268
|
+
function checkPermissions(dirPath) {
|
|
269
|
+
const result = { read: false, write: false, execute: false };
|
|
270
|
+
try { fs.accessSync(dirPath, fs.constants.R_OK); result.read = true; } catch { /* */ }
|
|
271
|
+
try { fs.accessSync(dirPath, fs.constants.W_OK); result.write = true; } catch { /* */ }
|
|
272
|
+
try { fs.accessSync(dirPath, fs.constants.X_OK); result.execute = true; } catch { /* */ }
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Rate Limiting (for CLI/server use) ───────────────────────────
|
|
277
|
+
|
|
278
|
+
class OperationThrottle {
|
|
279
|
+
constructor(maxOps = 1000, windowMs = 60000) {
|
|
280
|
+
this.maxOps = maxOps;
|
|
281
|
+
this.windowMs = windowMs;
|
|
282
|
+
this.operations = [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
canProceed() {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
this.operations = this.operations.filter(t => now - t < this.windowMs);
|
|
288
|
+
if (this.operations.length >= this.maxOps) {
|
|
289
|
+
return {
|
|
290
|
+
allowed: false,
|
|
291
|
+
retryAfterMs: this.windowMs - (now - this.operations[0]),
|
|
292
|
+
reason: `Rate limit: max ${this.maxOps} operations per ${this.windowMs / 1000}s`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
this.operations.push(now);
|
|
296
|
+
return { allowed: true };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
reset() {
|
|
300
|
+
this.operations = [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Integrity Verification ───────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a pre-operation snapshot for rollback verification
|
|
308
|
+
* @param {string} filePath - File to snapshot
|
|
309
|
+
* @returns {{ exists: boolean, size?: number, modified?: Date, path: string }}
|
|
310
|
+
*/
|
|
311
|
+
function createSnapshot(filePath) {
|
|
312
|
+
try {
|
|
313
|
+
const stat = fs.statSync(filePath);
|
|
314
|
+
return {
|
|
315
|
+
exists: true,
|
|
316
|
+
size: stat.size,
|
|
317
|
+
modified: stat.mtime,
|
|
318
|
+
path: path.resolve(filePath)
|
|
319
|
+
};
|
|
320
|
+
} catch {
|
|
321
|
+
return { exists: false, path: path.resolve(filePath) };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Verify a file matches a previous snapshot
|
|
327
|
+
* @param {string} filePath - File to verify
|
|
328
|
+
* @param {Object} snapshot - Previous snapshot
|
|
329
|
+
* @returns {{ matches: boolean, differences: string[] }}
|
|
330
|
+
*/
|
|
331
|
+
function verifySnapshot(filePath, snapshot) {
|
|
332
|
+
const differences = [];
|
|
333
|
+
try {
|
|
334
|
+
const stat = fs.statSync(filePath);
|
|
335
|
+
if (!snapshot.exists) {
|
|
336
|
+
differences.push('File now exists but previously did not');
|
|
337
|
+
} else {
|
|
338
|
+
if (stat.size !== snapshot.size) {
|
|
339
|
+
differences.push(`Size changed: ${snapshot.size} → ${stat.size}`);
|
|
340
|
+
}
|
|
341
|
+
if (stat.mtime.getTime() !== snapshot.modified.getTime()) {
|
|
342
|
+
differences.push('Modification time changed');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
if (snapshot.exists) {
|
|
347
|
+
differences.push('File no longer exists');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { matches: differences.length === 0, differences };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
validatePath,
|
|
355
|
+
isFileSafe,
|
|
356
|
+
isDirSafe,
|
|
357
|
+
isSubstrateCritical,
|
|
358
|
+
findNearestSubstrate,
|
|
359
|
+
sanitizeFilename,
|
|
360
|
+
sanitizeDirname,
|
|
361
|
+
canRead,
|
|
362
|
+
canWrite,
|
|
363
|
+
checkPermissions,
|
|
364
|
+
OperationThrottle,
|
|
365
|
+
createSnapshot,
|
|
366
|
+
verifySnapshot,
|
|
367
|
+
PROTECTED_DIRECTORIES,
|
|
368
|
+
PROTECTED_PREFIXES,
|
|
369
|
+
DANGEROUS_EXTENSIONS
|
|
370
|
+
};
|