550 lines
25 KiB
C#
550 lines
25 KiB
C#
|
|
using System;
|
|||
|
|
using System.Collections.Generic;
|
|||
|
|
using System.Collections.ObjectModel;
|
|||
|
|
using System.Globalization;
|
|||
|
|
using System.Linq;
|
|||
|
|
using System.Threading;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using System.Windows;
|
|||
|
|
using System.Windows.Data;
|
|||
|
|
using Microsoft.Win32;
|
|||
|
|
using MemScanEDR.Models;
|
|||
|
|
using MemScanEDR.Services;
|
|||
|
|
|
|||
|
|
namespace MemScanEDR;
|
|||
|
|
|
|||
|
|
// ─── Value Converter: List<string> → comma-separated string ───
|
|||
|
|
public class ListToStringConverter : IValueConverter
|
|||
|
|
{
|
|||
|
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|||
|
|
value is List<string> list && list.Count > 0 ? string.Join(", ", list) : "—";
|
|||
|
|
|
|||
|
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
|||
|
|
throw new NotSupportedException();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── Simple language helper ──────────────────────────────────
|
|||
|
|
public static class Lang
|
|||
|
|
{
|
|||
|
|
private static bool _isChinese = true;
|
|||
|
|
public static bool IsChinese => _isChinese;
|
|||
|
|
|
|||
|
|
public static void SetChinese(bool zh) => _isChinese = zh;
|
|||
|
|
|
|||
|
|
public static string T(string zh, string en) => _isChinese ? zh : en;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════════
|
|||
|
|
// MAIN WINDOW
|
|||
|
|
// ═══════════════════════════════════════════════════════════════
|
|||
|
|
public partial class MainWindow : Window
|
|||
|
|
{
|
|||
|
|
// ── Services ───────────────────────────────────────────────
|
|||
|
|
private readonly ProcessCollector _processCollector = new();
|
|||
|
|
private readonly MemoryScanner _memoryScanner = new();
|
|||
|
|
private readonly CeMcpManager _ceMcp = new();
|
|||
|
|
private AIAnalyzer? _aiAnalyzer;
|
|||
|
|
|
|||
|
|
// ── Application state ──────────────────────────────────────
|
|||
|
|
private AppSettings _settings = null!;
|
|||
|
|
private readonly ObservableCollection<ScanResult> _results = new();
|
|||
|
|
private readonly ObservableCollection<string> _scanLog = new();
|
|||
|
|
private List<Signature> _signatures = new();
|
|||
|
|
private CancellationTokenSource? _cts;
|
|||
|
|
private bool _isScanning;
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// CONSTRUCTOR
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
public MainWindow()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
InitializeComponent();
|
|||
|
|
|
|||
|
|
// Bind observable collections to DataGrids / ListBox
|
|||
|
|
DgAlerts.ItemsSource = _results;
|
|||
|
|
DgProcesses.ItemsSource = _results;
|
|||
|
|
LstScanLog.ItemsSource = _scanLog;
|
|||
|
|
|
|||
|
|
// Load settings & initialize AI analyzer
|
|||
|
|
_settings = AppSettings.Load();
|
|||
|
|
_aiAnalyzer = new AIAnalyzer(_settings.AI);
|
|||
|
|
|
|||
|
|
// Load signature database
|
|||
|
|
_signatures = _memoryScanner.LoadSignatures(_settings.RulesDir);
|
|||
|
|
|
|||
|
|
// Apply saved settings to the Settings view
|
|||
|
|
LoadSettingsToUI();
|
|||
|
|
ApplyLanguage();
|
|||
|
|
|
|||
|
|
AddLog(Lang.T("[init] MemScan-EDR 控制台已加载", "[init] MemScan-EDR console loaded"));
|
|||
|
|
|
|||
|
|
// Start CE-MCP server at launch (background)
|
|||
|
|
Task.Run(() =>
|
|||
|
|
{
|
|||
|
|
if (_settings.CeMcpPath != "disabled")
|
|||
|
|
{
|
|||
|
|
Dispatcher.Invoke(() => AddLog(Lang.T("[mcp] 正在启动 CE-MCP 服务器...", "[mcp] Starting CE-MCP server...")));
|
|||
|
|
var ok = _ceMcp.Start(_settings.CeMcpCommand, _settings.CeMcpPath == "auto" ? null : _settings.CeMcpPath);
|
|||
|
|
if (ok)
|
|||
|
|
{
|
|||
|
|
Thread.Sleep(1500);
|
|||
|
|
Dispatcher.Invoke(() => AddLog(Lang.T("[mcp] CE-MCP 服务器已启动", "[mcp] CE-MCP server started")));
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Dispatcher.Invoke(() => AddLog(Lang.T("[mcp] CE-MCP 服务器未找到,将跳过", "[mcp] CE-MCP server not found — skipped")));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
System.IO.File.AppendAllText(
|
|||
|
|
System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log"),
|
|||
|
|
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] MainWindow init error\n{ex}\n\n");
|
|||
|
|
System.Windows.MessageBox.Show($"Init error (see crash.log):\n\n{ex.Message}", "MemScan-EDR",
|
|||
|
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// LANGUAGE TOGGLE
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void BtnLangToggle_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
Lang.SetChinese(!Lang.IsChinese);
|
|||
|
|
ApplyLanguage();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ApplyLanguage()
|
|||
|
|
{
|
|||
|
|
// Title bar
|
|||
|
|
BtnLang.Content = Lang.T("EN", "中");
|
|||
|
|
|
|||
|
|
// Sidebar
|
|||
|
|
TbMonHeader.Text = Lang.T("监控", "MONITORING");
|
|||
|
|
TbSysHeader.Text = Lang.T("系统", "SYSTEM");
|
|||
|
|
TbNavDashboard.Text = Lang.T("仪表盘", "Dashboard");
|
|||
|
|
TbNavProcess.Text = Lang.T("进程列表", "Processes");
|
|||
|
|
TbNavScan.Text = Lang.T("扫描进度", "Scan Progress");
|
|||
|
|
TbNavSettings.Text = Lang.T("设置", "Settings");
|
|||
|
|
TbQuickScan.Text = Lang.T("快速扫描", "Quick Scan");
|
|||
|
|
TbQuickScanDesc.Text = Lang.T("立即扫描所有进程", "Scan all processes now");
|
|||
|
|
BtnStartScan.Content = Lang.T("开始扫描", "Start Scan");
|
|||
|
|
|
|||
|
|
// Dashboard
|
|||
|
|
TbDashboardTitle.Text = Lang.T("安全仪表盘", "Security Dashboard");
|
|||
|
|
TbStatTotal.Text = Lang.T("已扫描", "TOTAL SCANNED");
|
|||
|
|
TbStatTotalDesc.Text = Lang.T("进程", "Processes");
|
|||
|
|
TbStatHigh.Text = Lang.T("高风险", "HIGH RISK");
|
|||
|
|
TbStatHighDesc.Text = Lang.T("严重告警", "Critical Alerts");
|
|||
|
|
TbStatSusp.Text = Lang.T("可疑", "SUSPICIOUS");
|
|||
|
|
TbStatSuspDesc.Text = Lang.T("警告", "Warnings");
|
|||
|
|
TbStatSafe.Text = Lang.T("安全", "SAFE");
|
|||
|
|
TbStatSafeDesc.Text = Lang.T("正常", "Clean");
|
|||
|
|
TbRecentAlerts.Text = Lang.T("最近告警", "Recent Alerts");
|
|||
|
|
|
|||
|
|
// Scan view
|
|||
|
|
TbScanTitle.Text = Lang.T("扫描进度", "Scan Progress");
|
|||
|
|
BtnStopScan.Content = Lang.T("停止扫描", "Stop Scan");
|
|||
|
|
TbScanLog.Text = Lang.T("扫描日志", "Scan Log");
|
|||
|
|
|
|||
|
|
// Process view
|
|||
|
|
TbProcTitle.Text = Lang.T("扫描进程", "Scanned Processes");
|
|||
|
|
|
|||
|
|
// Settings
|
|||
|
|
TbSettingsTitle.Text = Lang.T("设置", "Settings");
|
|||
|
|
TbAiConfig.Text = Lang.T("AI 配置", "AI Configuration");
|
|||
|
|
TbAiProvider.Text = Lang.T("AI 提供商", "AI Provider");
|
|||
|
|
TbBaseUrl.Text = Lang.T("API 地址", "API Base URL");
|
|||
|
|
TbApiKey.Text = Lang.T("API 密钥", "API Key");
|
|||
|
|
TbModel.Text = Lang.T("模型名称", "Model Name");
|
|||
|
|
TbPathConfig.Text = Lang.T("路径配置", "Path Configuration");
|
|||
|
|
TbCeMcpPath.Text = Lang.T("CE-MCP 服务器路径", "CE-MCP Server Path");
|
|||
|
|
TbRulesDir.Text = Lang.T("扫描规则目录", "Scan Rules Directory");
|
|||
|
|
TbLogDir.Text = Lang.T("日志目录", "Log Directory");
|
|||
|
|
TbScanProfile.Text = Lang.T("扫描配置", "Scan Profile");
|
|||
|
|
BtnSaveSettings.Content = Lang.T("保存设置", "Save Settings");
|
|||
|
|
|
|||
|
|
// ComboBox items
|
|||
|
|
CmbAIProvider.Items[0] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("启发式规则 (无需 API)", "Heuristic (rule-based, no API)"), Tag = "Heuristic" };
|
|||
|
|
CmbAIProvider.Items[1] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("OpenAI 兼容 (OpenAI / DeepSeek / 通义等)", "OpenAI-Compatible (OpenAI / DeepSeek / etc.)"), Tag = "OpenAICompatible" };
|
|||
|
|
CmbAIProvider.Items[2] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("Claude (Anthropic API)", "Claude (Anthropic API)"), Tag = "Claude" };
|
|||
|
|
CmbAIProvider.Items[3] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("本地模型 (Ollama / LM Studio 等)", "Local (Ollama / LM Studio / etc.)"), Tag = "Local" };
|
|||
|
|
|
|||
|
|
CmbScanProfile.Items[0] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("快速 - 简单扫描", "fast - Quick scan"), Tag = "fast" };
|
|||
|
|
CmbScanProfile.Items[1] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("平衡 - 默认扫描深度", "balanced - Default scan depth"), Tag = "balanced" };
|
|||
|
|
CmbScanProfile.Items[2] = new System.Windows.Controls.ComboBoxItem { Content = Lang.T("深度 - 完整内存 + AI 分析", "deep - Full memory + AI analysis"), Tag = "deep" };
|
|||
|
|
|
|||
|
|
// Restore selected index
|
|||
|
|
LoadSettingsToUI();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// SIDEBAR NAVIGATION
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void NavChanged(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (DashboardView == null || ProcessView == null || ScanView == null || SettingsView == null)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
DashboardView.Visibility = Visibility.Collapsed;
|
|||
|
|
ProcessView.Visibility = Visibility.Collapsed;
|
|||
|
|
ScanView.Visibility = Visibility.Collapsed;
|
|||
|
|
SettingsView.Visibility = Visibility.Collapsed;
|
|||
|
|
|
|||
|
|
if (sender == NavDashboard) DashboardView.Visibility = Visibility.Visible;
|
|||
|
|
else if (sender == NavProcess) ProcessView.Visibility = Visibility.Visible;
|
|||
|
|
else if (sender == NavScan) ScanView.Visibility = Visibility.Visible;
|
|||
|
|
else if (sender == NavSettings) SettingsView.Visibility = Visibility.Visible;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// WINDOW CHROME BUTTONS
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void TitleBar_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (e.ClickCount == 2)
|
|||
|
|
BtnMaximize(sender, e);
|
|||
|
|
else
|
|||
|
|
DragMove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnMinimize(object sender, RoutedEventArgs e) =>
|
|||
|
|
WindowState = WindowState.Minimized;
|
|||
|
|
|
|||
|
|
private void BtnMaximize(object sender, RoutedEventArgs e) =>
|
|||
|
|
WindowState = WindowState == WindowState.Maximized
|
|||
|
|
? WindowState.Normal
|
|||
|
|
: WindowState.Maximized;
|
|||
|
|
|
|||
|
|
private void BtnClose(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
_cts?.Cancel();
|
|||
|
|
_ceMcp.Stop();
|
|||
|
|
Close();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// SCAN PROGRESS REPORTING MODEL
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private class ScanProgress
|
|||
|
|
{
|
|||
|
|
public string ProcessName { get; init; } = "";
|
|||
|
|
public string Message { get; init; } = "";
|
|||
|
|
public double Progress { get; init; }
|
|||
|
|
public ScanResult? Result { get; init; }
|
|||
|
|
public bool IsComplete { get; init; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// START SCAN
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private async void BtnStartScan_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
if (_isScanning) return;
|
|||
|
|
|
|||
|
|
_isScanning = true;
|
|||
|
|
_cts = new CancellationTokenSource();
|
|||
|
|
var ct = _cts.Token;
|
|||
|
|
|
|||
|
|
BtnStartScan.IsEnabled = false;
|
|||
|
|
BtnStopScan.IsEnabled = true;
|
|||
|
|
_scanLog.Clear();
|
|||
|
|
_results.Clear();
|
|||
|
|
UpdateStats();
|
|||
|
|
ScanProgressBar.Value = 0;
|
|||
|
|
LblCurrentProcess.Text = Lang.T("初始化中...", "Initializing...");
|
|||
|
|
|
|||
|
|
NavScan.IsChecked = true;
|
|||
|
|
|
|||
|
|
SetScanStatus(Lang.T("扫描中", "SCANNING"), true);
|
|||
|
|
AddLog(Lang.T($"[scan] 扫描开始 — {DateTime.Now:yyyy-MM-dd HH:mm:ss}", $"[scan] Scan started — {DateTime.Now:yyyy-MM-dd HH:mm:ss}"));
|
|||
|
|
|
|||
|
|
var progress = new Progress<ScanProgress>(OnScanProgress);
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await Task.Run(() => RunScanAsync(progress, ct), ct);
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
AddLog(Lang.T("[scan] 扫描已取消", "[scan] Scan cancelled by user"));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
AddLog($"[error] {ex.Message}");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
_isScanning = false;
|
|||
|
|
BtnStartScan.IsEnabled = true;
|
|||
|
|
BtnStopScan.IsEnabled = false;
|
|||
|
|
SetScanStatus(Lang.T("空闲", "IDLE"), false);
|
|||
|
|
AddLog(Lang.T($"[scan] 扫描完成 — {DateTime.Now:yyyy-MM-dd HH:mm:ss}", $"[scan] Scan completed — {DateTime.Now:yyyy-MM-dd HH:mm:ss}"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnStopScan_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
_cts?.Cancel();
|
|||
|
|
AddLog(Lang.T("[scan] 正在停止...", "[scan] Stop requested..."));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// CORE SCAN LOGIC (background)
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private async Task RunScanAsync(IProgress<ScanProgress> progress, CancellationToken ct)
|
|||
|
|
{
|
|||
|
|
// CE-MCP already started at launch — just check if it's running
|
|||
|
|
if (_settings.CeMcpPath != "disabled" && !_ceMcp.IsRunning)
|
|||
|
|
{
|
|||
|
|
progress.Report(new ScanProgress { Message = Lang.T("[mcp] CE-MCP 未运行,尝试启动...", "[mcp] CE-MCP not running, attempting start...") });
|
|||
|
|
_ceMcp.Start(_settings.CeMcpCommand, _settings.CeMcpPath == "auto" ? null : _settings.CeMcpPath);
|
|||
|
|
await Task.Delay(1000, ct);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Enumerate processes
|
|||
|
|
progress.Report(new ScanProgress { Message = Lang.T("[proc] 正在枚举进程...", "[proc] Enumerating processes...") });
|
|||
|
|
var processes = _processCollector.CollectAll();
|
|||
|
|
progress.Report(new ScanProgress { Message = Lang.T($"[proc] 发现 {processes.Count} 个进程", $"[proc] Found {processes.Count} processes") });
|
|||
|
|
|
|||
|
|
var total = processes.Count;
|
|||
|
|
var scanned = 0;
|
|||
|
|
|
|||
|
|
foreach (var proc in processes)
|
|||
|
|
{
|
|||
|
|
ct.ThrowIfCancellationRequested();
|
|||
|
|
|
|||
|
|
scanned++;
|
|||
|
|
var pct = (double)scanned / total * 100.0;
|
|||
|
|
|
|||
|
|
progress.Report(new ScanProgress
|
|||
|
|
{
|
|||
|
|
ProcessName = $"{proc.Name} (PID {proc.Pid})",
|
|||
|
|
Message = Lang.T($"[{scanned}/{total}] 扫描 {proc.Name} (PID {proc.Pid})...", $"[{scanned}/{total}] Scanning {proc.Name} (PID {proc.Pid})..."),
|
|||
|
|
Progress = pct
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var matches = _memoryScanner.ScanProcess(proc.Pid, _signatures);
|
|||
|
|
var suspiciousRegions = _memoryScanner.GetSuspiciousRegions(proc.Pid);
|
|||
|
|
var result = await _aiAnalyzer!.AnalyzeProcessAsync(proc, matches, suspiciousRegions);
|
|||
|
|
|
|||
|
|
var riskTag = result.RiskLevel switch
|
|||
|
|
{
|
|||
|
|
RiskLevel.High => Lang.T("高危", "HIGH"),
|
|||
|
|
RiskLevel.Suspicious => Lang.T("可疑", "SUSP"),
|
|||
|
|
_ => Lang.T("安全", "SAFE")
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
progress.Report(new ScanProgress
|
|||
|
|
{
|
|||
|
|
Message = $"[{scanned}/{total}] [{riskTag}] {proc.Name} score={result.RiskScore:F2} ({result.AIProvider})",
|
|||
|
|
Progress = pct,
|
|||
|
|
Result = result
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
progress.Report(new ScanProgress
|
|||
|
|
{
|
|||
|
|
Message = $"[warn] {proc.Name} — {ex.Message}",
|
|||
|
|
Progress = pct
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await Task.Delay(20, ct);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
progress.Report(new ScanProgress
|
|||
|
|
{
|
|||
|
|
Message = Lang.T($"[done] 扫描完成 — 共分析 {total} 个进程", $"[done] Scan complete — {total} processes analyzed"),
|
|||
|
|
Progress = 100,
|
|||
|
|
IsComplete = true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// PROGRESS CALLBACK (UI thread)
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void OnScanProgress(ScanProgress sp)
|
|||
|
|
{
|
|||
|
|
if (sp.Progress > 0)
|
|||
|
|
ScanProgressBar.Value = sp.Progress;
|
|||
|
|
|
|||
|
|
if (!string.IsNullOrEmpty(sp.ProcessName))
|
|||
|
|
LblCurrentProcess.Text = sp.ProcessName;
|
|||
|
|
|
|||
|
|
if (!string.IsNullOrEmpty(sp.Message))
|
|||
|
|
AddLog(sp.Message);
|
|||
|
|
|
|||
|
|
if (sp.Result != null)
|
|||
|
|
{
|
|||
|
|
_results.Add(sp.Result);
|
|||
|
|
UpdateStats();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (LstScanLog.Items.Count > 0)
|
|||
|
|
LstScanLog.ScrollIntoView(LstScanLog.Items[^1]);
|
|||
|
|
|
|||
|
|
if (sp.IsComplete)
|
|||
|
|
{
|
|||
|
|
LblCurrentProcess.Text = Lang.T("扫描完成", "Scan complete");
|
|||
|
|
LblProcCount.Text = Lang.T($" — {_results.Count} 条结果", $" — {_results.Count} results");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// UI HELPERS
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void AddLog(string message)
|
|||
|
|
{
|
|||
|
|
var ts = DateTime.Now.ToString("HH:mm:ss");
|
|||
|
|
_scanLog.Add($"{ts} {message}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void UpdateStats()
|
|||
|
|
{
|
|||
|
|
var total = _results.Count;
|
|||
|
|
var high = _results.Count(r => r.RiskLevel == RiskLevel.High);
|
|||
|
|
var susp = _results.Count(r => r.RiskLevel == RiskLevel.Suspicious);
|
|||
|
|
var safe = total - high - susp;
|
|||
|
|
|
|||
|
|
StatTotal.Text = total.ToString();
|
|||
|
|
StatHigh.Text = high.ToString();
|
|||
|
|
StatSuspicious.Text = susp.ToString();
|
|||
|
|
StatSafe.Text = safe.ToString();
|
|||
|
|
|
|||
|
|
if (high > 0)
|
|||
|
|
{
|
|||
|
|
LblDashboardStatus.Text = Lang.T($"● {high} 条告警", $"● {high} ALERT{(high > 1 ? "S" : "")}");
|
|||
|
|
LblDashboardStatus.Foreground = TryFindResource("AccentRedBrush") as System.Windows.Media.Brush;
|
|||
|
|
}
|
|||
|
|
else if (susp > 0)
|
|||
|
|
{
|
|||
|
|
LblDashboardStatus.Text = Lang.T($"● {susp} 条警告", $"● {susp} WARNING{(susp > 1 ? "S" : "")}");
|
|||
|
|
LblDashboardStatus.Foreground = TryFindResource("AccentYellowBrush") as System.Windows.Media.Brush;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
LblDashboardStatus.Text = Lang.T("● 监控中", "● MONITORING");
|
|||
|
|
LblDashboardStatus.Foreground = TryFindResource("AccentBlueBrush") as System.Windows.Media.Brush;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void SetScanStatus(string text, bool isScanning)
|
|||
|
|
{
|
|||
|
|
LblScanStatus.Text = $"● {text}";
|
|||
|
|
if (isScanning)
|
|||
|
|
{
|
|||
|
|
LblScanStatus.Foreground = TryFindResource("AccentBlueBrush") as System.Windows.Media.Brush;
|
|||
|
|
ScanStatusBadge.Background = TryFindResource("HighlightBlueBrush") as System.Windows.Media.Brush;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
LblScanStatus.Foreground = TryFindResource("AccentGreenBrush") as System.Windows.Media.Brush;
|
|||
|
|
ScanStatusBadge.Background = new System.Windows.Media.SolidColorBrush(
|
|||
|
|
System.Windows.Media.Color.FromRgb(0xf0, 0xfd, 0xf4));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
// SETTINGS VIEW
|
|||
|
|
// ═══════════════════════════════════════════════════════════
|
|||
|
|
private void LoadSettingsToUI()
|
|||
|
|
{
|
|||
|
|
CmbAIProvider.SelectedIndex = _settings.AI.Provider switch
|
|||
|
|
{
|
|||
|
|
AIProvider.OpenAICompatible => 1,
|
|||
|
|
AIProvider.Claude => 2,
|
|||
|
|
AIProvider.Local => 3,
|
|||
|
|
_ => 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
TxtApiKey.Password = _settings.AI.ApiKey;
|
|||
|
|
TxtBaseUrl.Text = _settings.AI.BaseUrl;
|
|||
|
|
TxtModel.Text = _settings.AI.Model;
|
|||
|
|
|
|||
|
|
TxtCeMcpPath.Text = _settings.CeMcpPath;
|
|||
|
|
TxtRulesDir.Text = _settings.RulesDir;
|
|||
|
|
TxtLogDir.Text = _settings.LogDir;
|
|||
|
|
|
|||
|
|
CmbScanProfile.SelectedIndex = _settings.ScanProfile switch
|
|||
|
|
{
|
|||
|
|
"fast" => 0,
|
|||
|
|
"deep" => 2,
|
|||
|
|
_ => 1
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnSaveSettings_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var providerTag = (CmbAIProvider.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "Heuristic";
|
|||
|
|
var provider = providerTag switch
|
|||
|
|
{
|
|||
|
|
"OpenAICompatible" => AIProvider.OpenAICompatible,
|
|||
|
|
"Claude" => AIProvider.Claude,
|
|||
|
|
"Local" => AIProvider.Local,
|
|||
|
|
_ => AIProvider.Heuristic
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_settings.AI = AIConfig.DefaultFor(provider);
|
|||
|
|
_settings.AI.ApiKey = TxtApiKey.Password;
|
|||
|
|
_settings.AI.BaseUrl = TxtBaseUrl.Text.Trim();
|
|||
|
|
_settings.AI.Model = TxtModel.Text.Trim();
|
|||
|
|
|
|||
|
|
_settings.CeMcpPath = TxtCeMcpPath.Text.Trim();
|
|||
|
|
_settings.RulesDir = TxtRulesDir.Text.Trim();
|
|||
|
|
_settings.LogDir = TxtLogDir.Text.Trim();
|
|||
|
|
|
|||
|
|
var profileTag = (CmbScanProfile.SelectedItem as System.Windows.Controls.ComboBoxItem)?.Tag?.ToString() ?? "balanced";
|
|||
|
|
_settings.ScanProfile = profileTag;
|
|||
|
|
|
|||
|
|
_settings.Save();
|
|||
|
|
|
|||
|
|
_aiAnalyzer = new AIAnalyzer(_settings.AI);
|
|||
|
|
_signatures = _memoryScanner.LoadSignatures(_settings.RulesDir);
|
|||
|
|
|
|||
|
|
AddLog(Lang.T("[settings] 设置已保存", "[settings] Settings saved"));
|
|||
|
|
|
|||
|
|
System.Windows.MessageBox.Show(
|
|||
|
|
Lang.T("设置已保存,AI 分析器和签名库已重新加载。", "Settings saved. AI analyzer and signatures reloaded."),
|
|||
|
|
"MemScan-EDR",
|
|||
|
|
MessageBoxButton.OK,
|
|||
|
|
MessageBoxImage.Information);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnBrowseCeMcp_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var dlg = new OpenFileDialog
|
|||
|
|
{
|
|||
|
|
Title = Lang.T("选择 CE-MCP 服务器脚本", "Select CE-MCP Server Script"),
|
|||
|
|
Filter = "Python (*.py)|*.py|All (*.*)|*.*"
|
|||
|
|
};
|
|||
|
|
if (dlg.ShowDialog() == true)
|
|||
|
|
TxtCeMcpPath.Text = dlg.FileName;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnBrowseRules_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var folder = BrowseFolder(Lang.T("选择扫描规则目录", "Select Scan Rules Directory"));
|
|||
|
|
if (folder != null) TxtRulesDir.Text = folder;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void BtnBrowseLog_Click(object sender, RoutedEventArgs e)
|
|||
|
|
{
|
|||
|
|
var folder = BrowseFolder(Lang.T("选择日志目录", "Select Log Directory"));
|
|||
|
|
if (folder != null) TxtLogDir.Text = folder;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string? BrowseFolder(string description)
|
|||
|
|
{
|
|||
|
|
var dlg = new OpenFolderDialog { Title = description };
|
|||
|
|
return dlg.ShowDialog() == true ? dlg.FolderName : null;
|
|||
|
|
}
|
|||
|
|
}
|