MemScan-EDR/src-csharp/MemScanEDR/MainWindow.xaml.cs

550 lines
25 KiB
C#
Raw Normal View History

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;
}
}