stella-coder 3.9.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/(/320/270/320/274/321/217 +0 -0
- package/.env.example +4 -0
- package/README.md +72 -0
- package/STELLA.md +6 -0
- package/antimalware/database.mjs +871 -0
- package/antimalware/index.mjs +8 -0
- package/antimalware/scanner.mjs +591 -0
- package/antimalware/ui.mjs +570 -0
- package/components.json +21 -0
- package/create_cats_pptx.py +121 -0
- package/eslint.config.mjs +16 -0
- package/hello.py +2 -0
- package/install.bat +35 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +11 -0
- package/package.json +58 -0
- package/pnpm-workspace.yaml +7 -0
- package/postcss.config.mjs +8 -0
- package/publish.mjs +52 -0
- package/stella-cli/banner.mjs +46 -0
- package/stella-cli/build.mjs +151 -0
- package/stella-cli/index.mjs +3073 -0
- package/stella-cli/markdown.mjs +100 -0
- package/stella-cli/sea-config.json +5 -0
- package/stella-cli/security.mjs +237 -0
- package/stella-cli/theme.mjs +89 -0
- package/stella-cli/tools.mjs +3145 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { startInteractive } from "./ui.mjs"
|
|
2
|
+
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════
|
|
4
|
+
// STELLAR ANTIVIRUS — Entry Point
|
|
5
|
+
// Full system scanner with Kaspersky-like UI
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
startInteractive()
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { execSync } from "node:child_process"
|
|
4
|
+
import { checkFileForMalware, computeFileHash, SKIP_DIRS, QUICK_SCAN_PATHS, QUARANTINE_DIR, isExcluded } from "./database.mjs"
|
|
5
|
+
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════
|
|
7
|
+
// STELLAR ANTIVIRUS — Full Scanner Engine
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
const SCAN_EXTENSIONS = new Set([
|
|
11
|
+
".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx",
|
|
12
|
+
".py", ".rb", ".php", ".pl", ".sh", ".bash",
|
|
13
|
+
".bat", ".cmd", ".ps1", ".psm1", ".psd1",
|
|
14
|
+
".exe", ".dll", ".sys", ".com", ".scr", ".pif",
|
|
15
|
+
".vbs", ".vbe", ".jse", ".wsf", ".wsh",
|
|
16
|
+
".hta", ".cpl", ".msi", ".msp", ".mst",
|
|
17
|
+
".doc", ".docm", ".xls", ".xlsm", ".ppt", ".pptm",
|
|
18
|
+
".pdf", ".rtf",
|
|
19
|
+
".jar", ".class", ".war",
|
|
20
|
+
".reg", ".inf",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
// Skip large files (over 10MB for speed)
|
|
24
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
// Known safe directories to skip
|
|
27
|
+
const SAFE_DIRS = new Set([
|
|
28
|
+
"Microsoft", "dotnet", "NuGet", "pip", "npm", "node-gyp",
|
|
29
|
+
"VisualStudio", "VS", "Windows Kits", "Windows Defender",
|
|
30
|
+
"Package Cache", "Installer", "apps", "packages",
|
|
31
|
+
"Windows SDK", "WindowsAppCertificationKit", "WindowsDesktopExtensionSDK",
|
|
32
|
+
"WindowsIoTExtensionSDK", "WindowsMobileExtensionSDK", "WindowsTeamExtensionSDK",
|
|
33
|
+
"Universal CRT", "WinRT Intellisense", "Kits", "WPT",
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
// Skip files from known safe publishers
|
|
37
|
+
const SAFE_FILE_PATTERNS = [
|
|
38
|
+
/^vs_/, /^Microsoft\.VisualStudio/, /^dump64/, /^msdia/, /^msvcp/,
|
|
39
|
+
/^vcruntime/, /^KernelTrace/, /^feedback/, /^Dia2Lib/, /^envdte/,
|
|
40
|
+
/^OSExtensions/, /^TraceRelogger/, /^msalruntime/, /^websocket-sharp/,
|
|
41
|
+
/^System\./, /^Microsoft\.Diagnostics/, /^Microsoft\.Identity/,
|
|
42
|
+
/^node-gyp/, /^Setup$/, /^setup\.exe$/, /^InstallCleanup/,
|
|
43
|
+
/^VSInstallerElevationService/, /^vswhere/, /^vs_installer/,
|
|
44
|
+
/^vs_installershell/, /^Microsoft\.VisualStudio\.Setup\./,
|
|
45
|
+
/^D3DCompiler/, /^PresentationNative/, /^wpfgfx/,
|
|
46
|
+
/^capCut/, /^ChromeSetup/, /^Claude/, /^Codex/, /^Docker/,
|
|
47
|
+
/^BlueStacks/, /^VC_redist/,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
// Safe path patterns (substring match)
|
|
51
|
+
const SAFE_PATH_PATTERNS = [
|
|
52
|
+
"\\Microsoft\\", "\\dotnet\\", "\\NuGet\\", "\\pip\\", "\\npm\\",
|
|
53
|
+
"\\node-gyp\\", "\\Visual Studio\\", "\\Windows Kits\\",
|
|
54
|
+
"\\Package Cache\\", "\\Installer\\", "\\Windows SDK\\",
|
|
55
|
+
"\\AppData\\Local\\Temp\\", "\\AppData\\Local\\Microsoft\\",
|
|
56
|
+
"\\AppData\\Roaming\\npm\\", "\\AppData\\Roaming\\pip\\",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════
|
|
60
|
+
// EXCLUSIONS
|
|
61
|
+
// ═══════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
let exclusions = []
|
|
64
|
+
|
|
65
|
+
export function loadExclusions(filepath) {
|
|
66
|
+
try {
|
|
67
|
+
if (fs.existsSync(filepath)) {
|
|
68
|
+
exclusions = fs.readFileSync(filepath, "utf8")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map(l => l.trim())
|
|
71
|
+
.filter(l => l && !l.startsWith("#"))
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function addExclusion(pattern) {
|
|
77
|
+
if (!exclusions.includes(pattern)) {
|
|
78
|
+
exclusions.push(pattern)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function removeExclusion(pattern) {
|
|
83
|
+
exclusions = exclusions.filter(e => e !== pattern)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getExclusions() {
|
|
87
|
+
return [...exclusions]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isExcludedPath(filepath) {
|
|
91
|
+
const normalized = filepath.replace(/\\/g, "/").toLowerCase()
|
|
92
|
+
for (const exc of exclusions) {
|
|
93
|
+
const pattern = exc.replace(/\\/g, "/").toLowerCase()
|
|
94
|
+
if (normalized.includes(pattern)) return true
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════
|
|
100
|
+
// FILE SCANNER
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
export function scanFile(filepath, options = {}) {
|
|
104
|
+
const { verbose = false, onFile = null } = options
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(filepath)) return { filepath, error: "Файл не найден" }
|
|
108
|
+
|
|
109
|
+
const stat = fs.statSync(filepath)
|
|
110
|
+
if (!stat.isFile()) return { filepath, error: "Не является файлом" }
|
|
111
|
+
|
|
112
|
+
if (stat.size > MAX_FILE_SIZE) return { filepath, error: `Файл слишком большой (${(stat.size / 1024 / 1024).toFixed(1)} MB)` }
|
|
113
|
+
|
|
114
|
+
if (isExcludedPath(filepath)) return { filepath, excluded: true }
|
|
115
|
+
|
|
116
|
+
// Skip known safe files
|
|
117
|
+
const basename = path.basename(filepath)
|
|
118
|
+
if (SAFE_FILE_PATTERNS.some(p => p.test(basename))) return { filepath, excluded: true }
|
|
119
|
+
|
|
120
|
+
// Skip safe path patterns
|
|
121
|
+
if (SAFE_PATH_PATTERNS.some(p => filepath.includes(p))) return { filepath, excluded: true }
|
|
122
|
+
|
|
123
|
+
if (onFile) onFile(filepath, stat.size)
|
|
124
|
+
|
|
125
|
+
const content = fs.readFileSync(filepath)
|
|
126
|
+
const result = checkFileForMalware(content, filepath)
|
|
127
|
+
result.size = stat.size
|
|
128
|
+
result.modified = stat.mtime
|
|
129
|
+
result.hash = computeFileHash(content)
|
|
130
|
+
|
|
131
|
+
return result
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return { filepath, error: e.message }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════
|
|
138
|
+
// FULL SYSTEM SCAN
|
|
139
|
+
// ═══════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
export function fullSystemScan(options = {}) {
|
|
142
|
+
const { verbose = false, onFile = null, onProgress = null, maxDepth = 20 } = options
|
|
143
|
+
const results = []
|
|
144
|
+
let fileCount = 0
|
|
145
|
+
|
|
146
|
+
function walk(currentPath, depth) {
|
|
147
|
+
if (depth > maxDepth) return
|
|
148
|
+
|
|
149
|
+
let entries
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
152
|
+
} catch { return }
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
156
|
+
if (SAFE_DIRS.has(entry.name)) continue
|
|
157
|
+
if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".config") continue
|
|
158
|
+
|
|
159
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
160
|
+
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
walk(fullPath, depth + 1)
|
|
163
|
+
} else if (entry.isFile()) {
|
|
164
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
165
|
+
if (SCAN_EXTENSIONS.has(ext) || !ext) {
|
|
166
|
+
fileCount++
|
|
167
|
+
if (onProgress) onProgress(fileCount, fullPath)
|
|
168
|
+
|
|
169
|
+
const result = scanFile(fullPath, { verbose, onFile })
|
|
170
|
+
results.push(result)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Scan all drives on Windows
|
|
177
|
+
const drives = ["C:", "D:", "E:", "F:"]
|
|
178
|
+
for (const drive of drives) {
|
|
179
|
+
const drivePath = drive + "\\"
|
|
180
|
+
if (fs.existsSync(drivePath)) {
|
|
181
|
+
walk(drivePath, 0)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { results, totalFiles: fileCount, scanType: "full" }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════
|
|
189
|
+
// QUICK SCAN
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════
|
|
191
|
+
|
|
192
|
+
export function quickScan(options = {}) {
|
|
193
|
+
const { verbose = false, onFile = null, onProgress = null, maxFiles = 2000 } = options
|
|
194
|
+
const results = []
|
|
195
|
+
let fileCount = 0
|
|
196
|
+
|
|
197
|
+
const username = process.env.USERNAME || process.env.USER || "user"
|
|
198
|
+
|
|
199
|
+
const scanPaths = QUICK_SCAN_PATHS.map(p =>
|
|
200
|
+
p.replace(/%USERNAME%/g, username)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
for (const scanPath of scanPaths) {
|
|
204
|
+
if (!fs.existsSync(scanPath)) continue
|
|
205
|
+
if (fileCount >= maxFiles) break
|
|
206
|
+
|
|
207
|
+
function walk(currentPath, depth) {
|
|
208
|
+
if (depth > 5 || fileCount >= maxFiles) return
|
|
209
|
+
|
|
210
|
+
let entries
|
|
211
|
+
try {
|
|
212
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
213
|
+
} catch { return }
|
|
214
|
+
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
217
|
+
if (SAFE_DIRS.has(entry.name)) continue
|
|
218
|
+
if (fileCount >= maxFiles) break
|
|
219
|
+
|
|
220
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
221
|
+
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
walk(fullPath, depth + 1)
|
|
224
|
+
} else if (entry.isFile()) {
|
|
225
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
226
|
+
if (SCAN_EXTENSIONS.has(ext) || !ext) {
|
|
227
|
+
fileCount++
|
|
228
|
+
if (onProgress) onProgress(fileCount, fullPath)
|
|
229
|
+
|
|
230
|
+
const result = scanFile(fullPath, { verbose, onFile })
|
|
231
|
+
results.push(result)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
walk(scanPath, 0)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { results, totalFiles: fileCount, scanType: "quick" }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ═══════════════════════════════════════════════════════════════
|
|
244
|
+
// CUSTOM PATH SCAN
|
|
245
|
+
// ═══════════════════════════════════════════════════════════════
|
|
246
|
+
|
|
247
|
+
export function scanPath(dirPath, options = {}) {
|
|
248
|
+
const { verbose = false, onFile = null, onProgress = null, maxDepth = 10 } = options
|
|
249
|
+
const results = []
|
|
250
|
+
let fileCount = 0
|
|
251
|
+
|
|
252
|
+
function walk(currentPath, depth) {
|
|
253
|
+
if (depth > maxDepth) return
|
|
254
|
+
|
|
255
|
+
let entries
|
|
256
|
+
try {
|
|
257
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
258
|
+
} catch { return }
|
|
259
|
+
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
262
|
+
if (entry.name.startsWith(".")) continue
|
|
263
|
+
|
|
264
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
265
|
+
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
walk(fullPath, depth + 1)
|
|
268
|
+
} else if (entry.isFile()) {
|
|
269
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
270
|
+
if (SCAN_EXTENSIONS.has(ext) || !ext) {
|
|
271
|
+
fileCount++
|
|
272
|
+
if (onProgress) onProgress(fileCount, fullPath)
|
|
273
|
+
|
|
274
|
+
const result = scanFile(fullPath, { verbose, onFile })
|
|
275
|
+
results.push(result)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (fs.existsSync(dirPath)) {
|
|
282
|
+
const stat = fs.statSync(dirPath)
|
|
283
|
+
if (stat.isFile()) {
|
|
284
|
+
results.push(scanFile(dirPath, { verbose, onFile }))
|
|
285
|
+
fileCount = 1
|
|
286
|
+
} else {
|
|
287
|
+
walk(dirPath, 0)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { results, totalFiles: fileCount, scanType: "custom" }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ═══════════════════════════════════════════════════════════════
|
|
295
|
+
// BOOT SECTOR / MBR SCANNER
|
|
296
|
+
// ═══════════════════════════════════════════════════════════════
|
|
297
|
+
|
|
298
|
+
export function scanBootSector(drive = "C:") {
|
|
299
|
+
try {
|
|
300
|
+
const devicePath = `\\\\.\\${drive}`
|
|
301
|
+
const fd = fs.openSync(devicePath, "r")
|
|
302
|
+
const buf = Buffer.alloc(512)
|
|
303
|
+
fs.readSync(fd, buf, 0, 512, 0)
|
|
304
|
+
fs.closeSync(fd)
|
|
305
|
+
|
|
306
|
+
const result = checkFileForMalware(buf, `${drive}\\MBR`)
|
|
307
|
+
result.scanType = "boot-sector"
|
|
308
|
+
return result
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return { filepath: `${drive}\\MBR`, error: `Не удалось прочитать MBR: ${e.message}` }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════
|
|
315
|
+
// MEMORY PROCESS SCANNER
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════
|
|
317
|
+
|
|
318
|
+
export function scanRunningProcesses() {
|
|
319
|
+
const results = []
|
|
320
|
+
|
|
321
|
+
// Known safe Windows processes
|
|
322
|
+
const SAFE_PROCESSES = new Set([
|
|
323
|
+
"WMIRegistrationService.exe", "WmiPrvSE.exe", "WmiApSrv.exe",
|
|
324
|
+
"svchost.exe", "services.exe", "lsass.exe", "wininit.exe",
|
|
325
|
+
"csrss.exe", "smss.exe", "winlogon.exe", "dwm.exe",
|
|
326
|
+
"explorer.exe", "taskhostw.exe", "conhost.exe",
|
|
327
|
+
"RuntimeBroker.exe", "ShellExperienceHost.exe", "StartMenuExperienceHost.exe",
|
|
328
|
+
"SearchUI.exe", "SearchIndexer.exe", "SearchProtocolHost.exe",
|
|
329
|
+
"SiHost.exe", "fontdrvhost.exe", "dllhost.exe",
|
|
330
|
+
"spoolsv.exe", "SearchFilterHost.exe", "WUDFHost.exe",
|
|
331
|
+
"msdtc.exe", "WerFault.exe", "WerMgr.exe",
|
|
332
|
+
"audiodg.exe", "mmsysse.exe", "ctfmon.exe",
|
|
333
|
+
"notepad.exe", "calc.exe", "mspaint.exe",
|
|
334
|
+
"chrome.exe", "firefox.exe", "msedge.exe", "opera.exe",
|
|
335
|
+
"Code.exe", "node.exe", "pnpm.exe", "npm.exe",
|
|
336
|
+
"ollama.exe", "ollama_llama_server.exe",
|
|
337
|
+
"powershell.exe", "pwsh.exe", "cmd.exe",
|
|
338
|
+
"WindowsTerminal.exe", "wt.exe",
|
|
339
|
+
"SecurityHealthService.exe", "SecurityHealthSystray.exe",
|
|
340
|
+
"MpCmdRun.exe", "MsMpEng.exe",
|
|
341
|
+
"NisSrv.exe", "WdNisSvc.exe",
|
|
342
|
+
"ApplicationFrameHost.exe", "SystemSettings.exe",
|
|
343
|
+
"TextInputHost.exe", "CompPkgSrv.exe",
|
|
344
|
+
"backgroundTaskHost.exe", "hctool.exe",
|
|
345
|
+
"KasperskyLab.exe", "avp.exe", "avpui.exe",
|
|
346
|
+
"OneDrive.exe", "onedrive.exe",
|
|
347
|
+
"Teams.exe", "Slack.exe", "Discord.exe",
|
|
348
|
+
"Spotify.exe", "Steam.exe", "EpicGamesLauncher.exe",
|
|
349
|
+
"vmwaretray.exe", "vmwareuser.exe", "vmtoolsd.exe",
|
|
350
|
+
"vboxservice.exe", "vboxtray.exe",
|
|
351
|
+
"Docker Desktop.exe", "com.docker.backend.exe",
|
|
352
|
+
"zoom.exe", "Zoom.exe",
|
|
353
|
+
])
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const output = execSync("tasklist /FO CSV /NH", { encoding: "utf8", timeout: 10000 })
|
|
357
|
+
const lines = output.split("\n").filter(l => l.trim())
|
|
358
|
+
|
|
359
|
+
for (const line of lines) {
|
|
360
|
+
const match = line.match(/"([^"]+)","(\d+)","([^"]+)"/)
|
|
361
|
+
if (match) {
|
|
362
|
+
const [, name, pid, session] = match
|
|
363
|
+
const isSafe = SAFE_PROCESSES.has(name)
|
|
364
|
+
const suspicious = !isSafe && /\b(mimikatz|meterpreter|cobaltstrike|inject|hook|keylog|rat|trojan|backdoor|rootkit|empire|covenant|sliver|brute ratel)\b/i.test(name)
|
|
365
|
+
results.push({
|
|
366
|
+
name,
|
|
367
|
+
pid: parseInt(pid),
|
|
368
|
+
memory: session,
|
|
369
|
+
suspicious,
|
|
370
|
+
severity: suspicious ? "critical" : "clean",
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch {}
|
|
375
|
+
return results
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ═══════════════════════════════════════════════════════════════
|
|
379
|
+
// QUARANTINE SYSTEM
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════
|
|
381
|
+
|
|
382
|
+
export function ensureQuarantine() {
|
|
383
|
+
if (!fs.existsSync(QUARANTINE_DIR)) {
|
|
384
|
+
fs.mkdirSync(QUARANTINE_DIR, { recursive: true })
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function quarantineFile(filepath) {
|
|
389
|
+
ensureQuarantine()
|
|
390
|
+
const filename = path.basename(filepath)
|
|
391
|
+
const hash = computeFileHash(fs.readFileSync(filepath))
|
|
392
|
+
const quarantinePath = path.join(QUARANTINE_DIR, `${hash}_${filename}.quarantine`)
|
|
393
|
+
const metaPath = quarantinePath + ".meta"
|
|
394
|
+
|
|
395
|
+
const metadata = {
|
|
396
|
+
originalPath: filepath,
|
|
397
|
+
hash,
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
size: fs.statSync(filepath).size,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fs.copyFileSync(filepath, quarantinePath)
|
|
403
|
+
fs.writeFileSync(metaPath, JSON.stringify(metadata, null, 2))
|
|
404
|
+
fs.unlinkSync(filepath)
|
|
405
|
+
|
|
406
|
+
return { quarantined: quarantinePath, metadata }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function restoreFromQuarantine(quarantinePath) {
|
|
410
|
+
const metaPath = quarantinePath + ".meta"
|
|
411
|
+
if (!fs.existsSync(metaPath)) return { error: "Метаданные не найдены" }
|
|
412
|
+
|
|
413
|
+
const metadata = JSON.parse(fs.readFileSync(metaPath, "utf8"))
|
|
414
|
+
const restoreDir = path.dirname(metadata.originalPath)
|
|
415
|
+
|
|
416
|
+
if (!fs.existsSync(restoreDir)) {
|
|
417
|
+
fs.mkdirSync(restoreDir, { recursive: true })
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fs.copyFileSync(quarantinePath, metadata.originalPath)
|
|
421
|
+
fs.unlinkSync(quarantinePath)
|
|
422
|
+
fs.unlinkSync(metaPath)
|
|
423
|
+
|
|
424
|
+
return { restored: metadata.originalPath }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function listQuarantine() {
|
|
428
|
+
ensureQuarantine()
|
|
429
|
+
const files = fs.readdirSync(QUARANTINE_DIR).filter(f => f.endsWith(".quarantine"))
|
|
430
|
+
return files.map(f => {
|
|
431
|
+
const metaPath = path.join(QUARANTINE_DIR, f + ".meta")
|
|
432
|
+
try {
|
|
433
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"))
|
|
434
|
+
return { file: f, ...meta }
|
|
435
|
+
} catch {
|
|
436
|
+
return { file: f, originalPath: "unknown" }
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ═══════════════════════════════════════════════════════════════
|
|
442
|
+
// REAL-TIME FILE MONITOR
|
|
443
|
+
// ═══════════════════════════════════════════════════════════════
|
|
444
|
+
|
|
445
|
+
let monitorActive = false
|
|
446
|
+
let monitorInterval = null
|
|
447
|
+
let fileHashes = new Map()
|
|
448
|
+
|
|
449
|
+
export function startRealTimeMonitor(dirPath, options = {}) {
|
|
450
|
+
const { interval = 5000, onThreat = null, onFileChange = null } = options
|
|
451
|
+
|
|
452
|
+
if (monitorActive) return { error: "Мониторинг уже запущен" }
|
|
453
|
+
|
|
454
|
+
monitorActive = true
|
|
455
|
+
const initialHashes = new Map()
|
|
456
|
+
|
|
457
|
+
// Build initial hash map
|
|
458
|
+
function buildHashMap(currentPath, depth) {
|
|
459
|
+
if (depth > 5) return
|
|
460
|
+
try {
|
|
461
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
462
|
+
for (const entry of entries) {
|
|
463
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
464
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
465
|
+
if (entry.isDirectory()) {
|
|
466
|
+
buildHashMap(fullPath, depth + 1)
|
|
467
|
+
} else if (entry.isFile()) {
|
|
468
|
+
try {
|
|
469
|
+
const stat = fs.statSync(fullPath)
|
|
470
|
+
initialHashes.set(fullPath, { size: stat.size, mtime: stat.mtimeMs })
|
|
471
|
+
} catch {}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch {}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
buildHashMap(dirPath, 0)
|
|
478
|
+
fileHashes = initialHashes
|
|
479
|
+
|
|
480
|
+
// Monitor loop
|
|
481
|
+
monitorInterval = setInterval(() => {
|
|
482
|
+
if (!monitorActive) return
|
|
483
|
+
|
|
484
|
+
const currentFiles = new Map()
|
|
485
|
+
|
|
486
|
+
function scanDir(currentPath, depth) {
|
|
487
|
+
if (depth > 5) return
|
|
488
|
+
try {
|
|
489
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
490
|
+
for (const entry of entries) {
|
|
491
|
+
if (SKIP_DIRS.has(entry.name)) continue
|
|
492
|
+
const fullPath = path.join(currentPath, entry.name)
|
|
493
|
+
if (entry.isDirectory()) {
|
|
494
|
+
scanDir(fullPath, depth + 1)
|
|
495
|
+
} else if (entry.isFile()) {
|
|
496
|
+
try {
|
|
497
|
+
const stat = fs.statSync(fullPath)
|
|
498
|
+
currentFiles.set(fullPath, { size: stat.size, mtime: stat.mtimeMs })
|
|
499
|
+
} catch {}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
scanDir(dirPath, 0)
|
|
506
|
+
|
|
507
|
+
// Check for new/modified files
|
|
508
|
+
for (const [filepath, info] of currentFiles) {
|
|
509
|
+
const prev = fileHashes.get(filepath)
|
|
510
|
+
if (!prev || prev.mtime !== info.mtime || prev.size !== info.size) {
|
|
511
|
+
if (onFileChange) onFileChange(filepath, prev ? "modified" : "new")
|
|
512
|
+
|
|
513
|
+
// Scan the file
|
|
514
|
+
const result = scanFile(filepath)
|
|
515
|
+
if (!result.clean && onThreat) {
|
|
516
|
+
onThreat(result)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check for deleted files
|
|
522
|
+
for (const [filepath] of fileHashes) {
|
|
523
|
+
if (!currentFiles.has(filepath)) {
|
|
524
|
+
if (onFileChange) onFileChange(filepath, "deleted")
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fileHashes = currentFiles
|
|
529
|
+
}, interval)
|
|
530
|
+
|
|
531
|
+
return { status: "started", dirPath, interval }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function stopRealTimeMonitor() {
|
|
535
|
+
monitorActive = false
|
|
536
|
+
if (monitorInterval) {
|
|
537
|
+
clearInterval(monitorInterval)
|
|
538
|
+
monitorInterval = null
|
|
539
|
+
}
|
|
540
|
+
fileHashes.clear()
|
|
541
|
+
return { status: "stopped" }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function getMonitorStatus() {
|
|
545
|
+
return { active: monitorActive, trackedFiles: fileHashes.size }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ═══════════════════════════════════════════════════════════════
|
|
549
|
+
// SCAN REPORT
|
|
550
|
+
// ═══════════════════════════════════════════════════════════════
|
|
551
|
+
|
|
552
|
+
export function generateReport(results, scanType = "full") {
|
|
553
|
+
const threats = results.filter(r => !r.clean && !r.error)
|
|
554
|
+
const warnings = results.filter(r => r.clean && r.warnings && r.warnings.length > 0)
|
|
555
|
+
const clean = results.filter(r => r.clean && (!r.warnings || r.warnings.length === 0))
|
|
556
|
+
const errors = results.filter(r => r.error)
|
|
557
|
+
|
|
558
|
+
const report = {
|
|
559
|
+
scanType,
|
|
560
|
+
timestamp: new Date().toISOString(),
|
|
561
|
+
summary: {
|
|
562
|
+
totalFiles: results.length,
|
|
563
|
+
threats: threats.length,
|
|
564
|
+
warnings: warnings.length,
|
|
565
|
+
clean: clean.length,
|
|
566
|
+
errors: errors.length,
|
|
567
|
+
},
|
|
568
|
+
threats: threats.map(r => ({
|
|
569
|
+
filepath: r.filepath,
|
|
570
|
+
score: r.score,
|
|
571
|
+
detections: [...r.threats, ...r.warnings],
|
|
572
|
+
})),
|
|
573
|
+
warnings: warnings.map(r => ({
|
|
574
|
+
filepath: r.filepath,
|
|
575
|
+
detections: r.warnings,
|
|
576
|
+
})),
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return report
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function saveReport(report, filepath) {
|
|
583
|
+
fs.writeFileSync(filepath, JSON.stringify(report, null, 2))
|
|
584
|
+
return filepath
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ═══════════════════════════════════════════════════════════════
|
|
588
|
+
// LEGACY COMPAT
|
|
589
|
+
// ═══════════════════════════════════════════════════════════════
|
|
590
|
+
|
|
591
|
+
export { scanPath as scanDirectory }
|