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

550 lines
25 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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