v1.2: Clean release with rewritten README, Chinese/English UI, AI anti-false-positive, CE-MCP auto-start

This commit is contained in:
Huang-158 2026-07-15 11:50:35 +08:00
commit 601773d487
21 changed files with 2741 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Build output
**/bin/
**/obj/
**/publish/
**/data/
**/logs/
**/dumps/
**/reports/
*.exe
*.dll
*.pdb
*.cache
# IDE
.vs/
.vscode/
*.user
*.suo
# OS
Thumbs.db
Desktop.ini
.DS_Store
# Python (legacy)
__pycache__/
*.pyc
venv/
dist/
build/
*.spec

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 MemScan-EDR Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

201
README.md Normal file
View File

@ -0,0 +1,201 @@
# MemScan-EDR
> **Open-Source, Lightweight, Memory-Focused Windows Endpoint Detection & Response (EDR) Tool**
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![.NET 8](https://img.shields.io/badge/.NET-8.0-purple.svg)](https://dotnet.microsoft.com/)
[![Platform: Windows](https://img.shields.io/badge/Platform-Windows%2010%2F11-lightgrey.svg)](https://www.microsoft.com/windows)
[![CS-WPF](https://img.shields.io/badge/UI-WPF-green.svg)](https://github.com/dotnet/wpf)
MemScan-EDR scans all running processes, inspects memory for shellcode/injection/C2 patterns, and uses AI (OpenAI / Claude / local models) to classify threats in real time. Built with C# WPF for zero-dependency deployment as a single `.exe`.
---
## Screenshots
| Dashboard | Scan Progress | Settings |
|-----------|--------------|----------|
| Real-time risk dashboard with live process statistics | Real-time scan log with per-process results | Multi-provider AI configuration panel |
---
## Features
### Core Capabilities
| Capability | Description |
|------------|-------------|
| **Process Enumeration** | Full process list with PID, name, path, parent tree, command line, digital signature, thread count, memory usage |
| **Memory Scanning** | `VirtualQueryEx` traversal of every committed region; detects RWX private memory, unbacked executable pages |
| **Signature Matching** | 12 built-in detection rules: NOP sled, CALL/POP EIP, ROR13 hash, CobaltStrike Beacon, Meterpreter, ReflectiveLoader, Mimikatz, etc. |
| **AI Threat Classification** | Multi-provider support (OpenAI-compatible / Claude / Ollama / LM Studio) + heuristic fallback engine |
| **CE-MCP Integration** | Auto-launch Cheat Engine MCP server for kernel-level memory operations |
| **Chinese / English UI** | One-click language toggle; all labels, buttons, and alerts switch instantly |
| **Single-File EXE** | `dotnet publish` produces a self-contained `MemScan-EDR.exe` — no .NET runtime needed on target |
### AI Providers
| Provider | Description | Base URL Example |
|----------|-------------|-----------------|
| **OpenAI-Compatible** | OpenAI / DeepSeek / Qwen / Zhipu / any OpenAI-format API | `https://api.openai.com/v1` |
| **Claude** | Anthropic Claude via Messages API | `https://api.anthropic.com` |
| **Local** | Ollama / LM Studio / vLLM / llama.cpp server | `http://127.0.0.1:11434` |
| **Heuristic** | Built-in rule engine (no API key needed) | N/A |
### Built-in Signatures
| Signature | Category | Severity |
|-----------|----------|----------|
| `SHELLCODE_NOP_SLED` | Shellcode | Medium |
| `SHELLCODE_GETEIP` | Shellcode | Medium |
| `SHELLCODE_ROR13` | Shellcode | Medium |
| `C2_COBALTSTRIKE` | C2 | High |
| `C2_METERPRETER` | C2 | Medium |
| `INJECTION_REFLECTIVE` | Injection | High |
| `INJECTION_CRT` | Injection | Medium |
| `INJECTION_VAE` | Injection | Medium |
| `MALWARE_MIMIKATZ` | Malware | High |
| `CRYPTO_MINER` | Crypto | High |
| `RANSOM_NOTE` | Malware | High |
| `PS_DOWNLOAD` | Malware | Medium |
---
## Architecture
```
TitleBar ── Window Chrome (Min/Max/Close, Language Toggle)
Sidebar ── Navigation (Dashboard / Processes / Scan / Settings)
Content ── 4 Views (WPF Grid Visibility switching)
Services:
ProcessCollector ── CreateToolhelp32Snapshot + WMI → ProcessInfo[]
MemoryScanner ── VirtualQueryEx + signature walk → SignatureMatch[]
AIAnalyzer ── HTTP → OpenAI/Claude/Ollama API → ScanResult
CeMcpManager ── Process.Start → python server.py
```
Data flows:
```
[Start Scan] → ProcessCollector.CollectAll()
→ for each ProcessInfo:
→ MemoryScanner.ScanProcess() → signature matches
→ MemoryScanner.GetSuspiciousRegions() → RWX private regions
→ QuickPreScore() — skip API if score < 0.05
→ AIAnalyzer.AnalyzeProcessAsync() → ScanResult
→ API call with structured prompt
→ ParseResponse() → RiskLevel + Score + Indicators
→ Update DataGrid / Stats in real-time via IProgress<>
```
---
## Quick Start
### Prerequisites
- Windows 10 / 11 (x64)
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (for building)
- Administrator privileges (for memory scanning)
### Build
```powershell
# Clone
git clone https://github.com/Huang-158/MemScan-EDR.git
cd MemScan-EDR\src-csharp
# Build (Debug)
dotnet build MemScanEDR.sln
# Publish single-file EXE (Release)
dotnet publish MemScanEDR\MemScanEDR.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
# Run
.\publish\MemScan-EDR.exe
```
Or use the convenience script:
```powershell
.\scripts\build_csharp.bat
```
### CE-MCP Setup (Optional)
CE-MCP provides deeper memory scanning via Cheat Engine. The application auto-starts it at launch if `server.py` is found.
1. Place [ce-mcp-server](https://github.com/Huang-158/ce-mcp-server) next to the project
2. Or configure the path in Settings → CE-MCP Server Path
3. Set to `disabled` to run without CE-MCP
### AI Configuration
Open Settings panel in the application:
1. Select provider: Heuristic / OpenAI-Compatible / Claude / Local
2. Enter API Base URL (pre-filled defaults for each provider)
3. Enter API Key (stored locally in `data/settings.json`)
4. Enter Model name (e.g. `gpt-4o-mini`, `claude-3-5-sonnet`, `llama3`)
5. Click Save
---
## Project Structure
```
MemScan-EDR/
├── src-csharp/
│ ├── MemScanEDR.sln
│ └── MemScanEDR/
│ ├── MemScanEDR.csproj # .NET 8 WPF project
│ ├── app.manifest # UAC asInvoker
│ ├── App.xaml / App.xaml.cs # Application entry + crash logging
│ ├── MainWindow.xaml # WPF UI (white theme, 4 views)
│ ├── MainWindow.xaml.cs # UI logic + scan orchestrator + Lang helper
│ ├── Models/
│ │ ├── ProcessInfo.cs # Data models (ProcessInfo, ScanResult, Signature, etc.)
│ │ ├── AIConfig.cs # AI provider enum + config class
│ │ └── AppSettings.cs # JSON persistence (settings.json)
│ └── Services/
│ ├── ProcessCollector.cs # P/Invoke process enumeration + WMI command lines
│ ├── MemoryScanner.cs # VirtualQueryEx scanning + 12 built-in signatures
│ ├── AIAnalyzer.cs # Multi-provider AI + heuristic fallback + pre-filter
│ └── CeMcpManager.cs # CE-MCP subprocess lifecycle
├── rules/
│ ├── shellcode.yar # YARA shellcode rules
│ ├── injection.yar # YARA injection rules
│ └── malware_strings.yar # YARA malware string rules
├── scripts/
│ └── build_csharp.bat # Build + publish shortcut
├── LICENSE
└── README.md
```
---
## How AI Analysis Works
The AI analyzer uses a carefully designed prompt to reduce false positives:
1. **Pre-filter**: Processes with zero suspicious indicators (signed, system path, no RWX memory, no signature matches) are marked `Safe` without any API call — saving cost and time.
2. **Structured Prompt**: Each process gets a formatted report including:
- Process metadata (PID, name, path, parent, command line, signer)
- Network connections (protocol, remote address, state)
- Memory signature matches (category, severity, description)
- Suspicious memory regions (RWX private, unbacked executable)
3. **Safe-by-Default Policy**: The AI is instructed that >95% of processes are benign. It requires **multiple concurring indicators** to elevate risk level — a single RWX region or signature match alone is not enough.
4. **Heuristic Fallback**: If the AI API is unavailable, a weighted heuristic engine takes over with conservative thresholds (≥0.50 for suspicious, ≥0.75 for high).
---
## Disclaimer
> **Third-Party / Non-Affiliation Statement**
>
> This project is not affiliated with Cheat Engine, its original author Dark Byte, or any AI provider (OpenAI, Anthropic, etc.).
>
> **Legal Use Statement**
>
> This tool is intended for **legitimate security research, education, and authorized security assessments only**. Unauthorized use against systems you do not own or have permission to test is illegal. Users are responsible for complying with all applicable laws.
---
## License
MIT License — see [LICENSE](LICENSE) for full text.

89
rules/injection.yar Normal file
View File

@ -0,0 +1,89 @@
/*
* MemScan-EDR 进程注入检测规则
*/
rule Remote_Thread_Creation {
meta:
description = "检测远程线程注入相关API字符串"
author = "MemScan-EDR"
severity = "medium"
category = "injection"
strings:
$createremotethread = "CreateRemoteThread" nocase
$rtlcreateuserthread = "RtlCreateUserThread" nocase
$ntcreatethreadex = "NtCreateThreadEx" nocase
condition:
any of them
}
rule DLL_Injection_APC {
meta:
description = "检测 APC 注入 (QueueUserAPC) 特征"
author = "MemScan-EDR"
severity = "medium"
category = "injection"
strings:
$queueuserapc = "QueueUserAPC" nocase
$ntqueueapcthread = "NtQueueApcThread" nocase
$setthreadcontext = "SetThreadContext" nocase
condition:
any of them
}
rule Process_DLL_Injection {
meta:
description = "检测 DLL 注入常用 API"
author = "MemScan-EDR"
severity = "medium"
category = "injection"
strings:
$virtualallocex = "VirtualAllocEx" nocase
$writeprocessmemory = "WriteProcessMemory" nocase
$loadlibrarya = "LoadLibraryA" nocase
$getprocaddress = "GetProcAddress" nocase
condition:
($virtualallocex and $writeprocessmemory) or $loadlibrarya
}
rule Process_Hollowing {
meta:
description = "检测 Process Hollowing 特征"
author = "MemScan-EDR"
severity = "high"
category = "injection"
strings:
$createprocess = "CreateProcess" nocase
$zwunmap = "ZwUnmapViewOfSection" nocase
$ntunmap = "NtUnmapViewOfSection" nocase
$rtluserprocess = "RtlUserProcessParameters" nocase
condition:
$createprocess and ($zwunmap or $ntunmap)
}
rule Atom_Bombing {
meta:
description = "检测 Atom Bombing 注入技术"
author = "MemScan-EDR"
severity = "high"
category = "injection"
strings:
$globaladd = "GlobalAddAtom" nocase
$globalget = "GlobalGetAtomName" nocase
$ntqueueapc = "NtQueueApcThread" nocase
condition:
2 of them
}
rule Early_Bird_APC {
meta:
description = "检测 Early Bird APC 注入"
author = "MemScan-EDR"
severity = "high"
category = "injection"
strings:
$create_suspended = "CREATE_SUSPENDED" nocase
$queueuserapc = "QueueUserAPC" nocase
$resumethread = "ResumeThread" nocase
condition:
2 of them
}

119
rules/malware_strings.yar Normal file
View File

@ -0,0 +1,119 @@
/*
* MemScan-EDR 恶意字符串检测规则
* 覆盖 C2 通信、勒索软件、挖矿等常见恶意字符串模式
*/
rule C2_Domain_Patterns {
meta:
description = "常见 C2 域名/URL 模式"
author = "MemScan-EDR"
severity = "high"
category = "c2"
strings:
$beacon_c2 = ".php?id=" ascii
$api_req = "/api/v1/beacon" nocase
$c2_uri = "/index.asp?id=" ascii
$heartbeat = "heartbeat" nocase
$check_in = "/checkin" nocase
$pulse = "/pulse" nocase
condition:
any of them
}
rule Ransomware_Strings {
meta:
description = "勒索软件常见字符串模式"
author = "MemScan-EDR"
severity = "high"
category = "malware"
strings:
$ransom = "ransom" nocase
$decrypt = "decrypt" nocase
$bitcoin = "bitcoin" nocase
$monero = "monero" nocase
$payment = "payment" nocase
$encrypted = "YOUR_FILES_ARE_ENCRYPTED" nocase
condition:
any of them
}
rule Crypto_Mining_Pool {
meta:
description = "加密货币挖矿池 URL 模式"
author = "MemScan-EDR"
severity = "high"
category = "crypto"
strings:
$stratum = "stratum+tcp://" ascii
$xmrig = "xmrig" nocase
$pool_url = "pool." nocase
$mining_proxy = "nicehash" nocase
$worker = "worker" nocase
condition:
any of them
}
rule Keylogger_Strings {
meta:
description = "键盘记录器常见字符串"
author = "MemScan-EDR"
severity = "high"
category = "malware"
strings:
$hook = "SetWindowsHookEx" nocase
$getkeystate = "GetAsyncKeyState" nocase
$keylog = "keylog" nocase
$clipboard = "GetClipboardData" nocase
condition:
2 of them
}
rule Credential_Theft {
meta:
description = "凭证窃取工具特征字符串"
author = "MemScan-EDR"
severity = "high"
category = "malware"
strings:
$mimikatz = "mimikatz" nocase
$sekurlsa = "sekurlsa" nocase
$lsa_dump = "lsadump" nocase
$wdigest = "wdigest" nocase
$kerberos = "kerberos" nocase
$logonpasswords = "logonpasswords" nocase
condition:
any of them
}
rule Reverse_Shell_Commands {
meta:
description = "反弹 Shell 常见命令"
author = "MemScan-EDR"
severity = "medium"
category = "c2"
strings:
$bash = "/bin/sh" ascii
$cmd = "cmd.exe" nocase
$powershell = "powershell.exe" nocase
$ncat = "nc " ascii
$pipe = "mkfifo" ascii
condition:
any of them
}
rule Credential_Dump_Tools {
meta:
description = "常见凭证提取工具名称"
author = "MemScan-EDR"
severity = "high"
category = "malware"
strings:
$pwdump = "pwdump" nocase
$fgdump = "fgdump" nocase
$cachedump = "cachedump" nocase
$samdump = "samdump" nocase
$procdump = "procdump" nocase
$lsass = "lsass" nocase
condition:
any of them
}

121
rules/shellcode.yar Normal file
View File

@ -0,0 +1,121 @@
/*
* MemScan-EDR Shellcode 检测规则
* 覆盖常见 C2 框架、注入技术、恶意 Shellcode 特征
*/
rule Meterpreter_Reverse_TCP_Shellcode {
meta:
description = "检测 Metasploit Meterpreter reverse_tcp shellcode"
author = "MemScan-EDR"
severity = "high"
category = "shellcode"
reference = "https://github.com/rapid7/metasploit-framework"
strings:
$stager = "WS2_32.dll" nocase
$port_loop = { 66 B8 [4] 50 6A 02 6A }
$wsastartup = { B8 [4] 50 6A [1-2] 6A }
$connect_pattern = { 6A 00 6A 00 6A 00 6A 06 6A 01 6A 02 }
condition:
any of them
}
rule CobaltStrike_Beacon_Config {
meta:
description = "检测 CobaltStrike Beacon 配置特征"
author = "MemScan-EDR"
severity = "high"
category = "c2"
strings:
$beacon_x86 = { 00 00 00 00 [16] 00 00 00 00 00 01 00 01 }
$beacon_x64 = { 00 00 00 00 00 00 00 00 [32] 00 01 00 01 00 }
$sleep_mask = { 00 00 00 00 [8] 00 00 00 00 }
$config_header = { 00 01 00 01 [16] }
condition:
any of them
}
rule Reflective_DLL_Injection {
meta:
description = "检测 Reflective DLL Injection 特征"
author = "MemScan-EDR"
severity = "high"
category = "injection"
strings:
$reflectiveloader = "ReflectiveLoader" nocase
$reflective_dll = "reflective_dll" nocase
$pe_in_memory = "MZ" at 0
$getprocaddress_hash = { 8B [1-2] E8 [4] 01 C1 [1-2] [1-2] 85 }
condition:
($reflectiveloader or $reflective_dll) and $pe_in_memory
}
rule Shellcode_NOP_Sled {
meta:
description = "检测 NOP sled (连续 0x90 指令)"
author = "MemScan-EDR"
severity = "medium"
category = "shellcode"
strings:
$nop_sled = { 90 90 90 90 90 90 90 90 90 90 90 90 }
$nop_int3_sled = { CC CC CC CC CC CC CC CC }
condition:
$nop_sled or $nop_int3_sled
}
rule Shellcode_GetEIP_Technique {
meta:
description = "检测获取 EIP/EIP 的 Shellcode 技术"
author = "MemScan-EDR"
severity = "medium"
category = "shellcode"
strings:
$call_pop = { E8 00 00 00 00 58 } // call $+5; pop eax
$call_pop2 = { E8 00 00 00 00 59 } // call $+5; pop ecx
$fs_teb = { 64 A1 30 00 00 00 } // mov eax, fs:[0x30] (PEB x86)
$gs_teb = { 65 48 8B [1-2] 60 00 00 00 } // mov reg, gs:[0x60] (PEB x64)
condition:
any of them
}
rule Shellcode_API_Hash_ROR13 {
meta:
description = "检测 ROR13 API 哈希解析模式"
author = "MemScan-EDR"
severity = "medium"
category = "shellcode"
strings:
$ror13_x86 = { C1 C8 0D } // ror eax, 0xD
$ror13_x64 = { 41 C1 C8 0D } // ror r8d, 0xD
$rol13_x86 = { C1 C0 0D } // rol eax, 0xD
condition:
any of them
}
rule Sliver_Implant {
meta:
description = "检测 Sliver C2 框架 implant 特征"
author = "MemScan-EDR"
severity = "high"
category = "c2"
strings:
$sliver = "sliver" nocase
$mtls = "mtls" nocase
$wg = "wireguard" nocase
$grpc = "gRPC" nocase
condition:
any of them
}
rule Process_Hollowing_Indicator {
meta:
description = "检测 Process Hollowing 特征"
author = "MemScan-EDR"
severity = "high"
category = "injection"
strings:
$ntunmap = "NtUnmapViewOfSection" nocase
$create_suspended = { 00 00 00 00 00 00 00 00 [8] 04 00 00 00 }
$pe_imagebase = { 00 00 40 00 00 00 00 00 } // IMAGE_BASE 0x400000
condition:
any of them
}

61
scripts/build_csharp.bat Normal file
View File

@ -0,0 +1,61 @@
@echo off
setlocal
cd /d "%~dp0.."
echo =======================================================
echo MemScan-EDR C# Build
echo =======================================================
echo.
where dotnet >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo [ERROR] .NET SDK not found!
echo.
echo Install .NET 8 SDK from:
echo https://dotnet.microsoft.com/download/dotnet/8.0
echo.
pause
exit /b 1
)
dotnet --version
echo.
echo [1/3] Restoring NuGet packages...
dotnet restore src-csharp\MemScanEDR.sln
if %ERRORLEVEL% NEQ 0 (
echo [FAIL] Restore failed
pause
exit /b 1
)
echo.
echo [2/3] Building Release (single-file exe)...
dotnet publish src-csharp\MemScanEDR\MemScanEDR.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o dist-csharp
if %ERRORLEVEL% NEQ 0 (
echo [FAIL] Build failed
pause
exit /b 1
)
echo.
echo [3/3] Copying resources...
if exist "rules" xcopy /E /I /Y "rules" "dist-csharp\rules" >nul
if exist "config" xcopy /E /I /Y "config" "dist-csharp\config" >nul
if exist "src\ce_mcp_server\server.py" copy /Y "src\ce_mcp_server\server.py" "dist-csharp\src\ce_mcp_server\server.py" >nul
echo.
echo =======================================================
echo BUILD SUCCESS!
echo.
if exist "dist-csharp\MemScan-EDR.exe" (
echo Output: dist-csharp\MemScan-EDR.exe
dir "dist-csharp\MemScan-EDR.exe" 2>nul | findstr "MemScan"
)
echo.
echo Copy dist-csharp\ folder to target machine.
echo Requires .NET 8 runtime OR self-contained exe.
echo =======================================================
echo.
pause

22
src-csharp/MemScanEDR.sln Normal file
View File

@ -0,0 +1,22 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemScanEDR", "MemScanEDR\MemScanEDR.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,5 @@
<Application x:Class="MemScanEDR.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>

View File

@ -0,0 +1,55 @@
using System;
using System.IO;
using System.Windows;
using System.Windows.Threading;
namespace MemScanEDR
{
public partial class App : Application
{
private static readonly string LogPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "crash.log");
protected override void OnStartup(StartupEventArgs e)
{
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException;
base.OnStartup(e);
try
{
Directory.CreateDirectory("data");
Directory.CreateDirectory("logs");
Directory.CreateDirectory("dumps");
Directory.CreateDirectory("reports");
}
catch (Exception ex)
{
LogError("Directory creation failed", ex);
}
}
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
LogError("UI Exception", e.Exception);
MessageBox.Show($"Error logged to crash.log:\n\n{e.Exception.Message}", "MemScan-EDR",
MessageBoxButton.OK, MessageBoxImage.Error);
e.Handled = true;
}
private void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
LogError("Fatal Exception", e.ExceptionObject as Exception);
}
private static void LogError(string context, Exception? ex)
{
try
{
var msg = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {context}\n{ex}\n\n";
File.AppendAllText(LogPath, msg);
}
catch { }
}
}
}

View File

@ -0,0 +1,489 @@
<Window x:Class="MemScanEDR.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MemScanEDR"
Title="MemScan-EDR - Endpoint Detection &amp; Response"
Height="750" Width="1200"
MinHeight="650" MinWidth="1000"
WindowStartupLocation="CenterScreen"
WindowStyle="None" AllowsTransparency="True"
Background="Transparent">
<Window.Resources>
<Color x:Key="BgRoot">#f0f2f5</Color>
<Color x:Key="BgSidebar">#1e293b</Color>
<Color x:Key="BgContent">#ffffff</Color>
<Color x:Key="BgCard">#ffffff</Color>
<Color x:Key="BgInput">#f1f5f9</Color>
<Color x:Key="BorderDim">#e2e8f0</Color>
<Color x:Key="TextPrimary">#1e293b</Color>
<Color x:Key="TextSecondary">#64748b</Color>
<Color x:Key="TextDim">#94a3b8</Color>
<Color x:Key="AccentRed">#ef4444</Color>
<Color x:Key="AccentBlue">#3b82f6</Color>
<Color x:Key="AccentGreen">#22c55e</Color>
<Color x:Key="AccentYellow">#f59e0b</Color>
<Color x:Key="HighlightBlue">#eff6ff</Color>
<Color x:Key="SidebarText">#cbd5e1</Color>
<Color x:Key="SidebarTextDim">#64748b</Color>
<Color x:Key="SidebarHighlight">#334155</Color>
<SolidColorBrush x:Key="BgRootBrush" Color="{StaticResource BgRoot}"/>
<SolidColorBrush x:Key="BgSidebarBrush" Color="{StaticResource BgSidebar}"/>
<SolidColorBrush x:Key="BgContentBrush" Color="{StaticResource BgContent}"/>
<SolidColorBrush x:Key="BgCardBrush" Color="{StaticResource BgCard}"/>
<SolidColorBrush x:Key="BgInputBrush" Color="{StaticResource BgInput}"/>
<SolidColorBrush x:Key="BorderDimBrush" Color="{StaticResource BorderDim}"/>
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}"/>
<SolidColorBrush x:Key="TextDimBrush" Color="{StaticResource TextDim}"/>
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}"/>
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}"/>
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}"/>
<SolidColorBrush x:Key="AccentYellowBrush" Color="{StaticResource AccentYellow}"/>
<SolidColorBrush x:Key="HighlightBlueBrush" Color="{StaticResource HighlightBlue}"/>
<SolidColorBrush x:Key="SidebarTextBrush" Color="{StaticResource SidebarText}"/>
<SolidColorBrush x:Key="SidebarTextDimBrush" Color="{StaticResource SidebarTextDim}"/>
<SolidColorBrush x:Key="SidebarHighlightBrush" Color="{StaticResource SidebarHighlight}"/>
<local:ListToStringConverter x:Key="ListToStringConv"/>
<!-- Nav Button -->
<Style x:Key="NavBtn" TargetType="RadioButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{StaticResource SidebarTextBrush}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontFamily" Value="Segoe UI, Microsoft YaHei"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Margin" Value="8,1"/>
<Setter Property="Padding" Value="14,0"/>
<Setter Property="GroupName" Value="Nav"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" CornerRadius="8" Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}" Margin="{TemplateBinding Margin}">
<ContentPresenter VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SidebarHighlightBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource AccentBlueBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Primary Button -->
<Style x:Key="PrimaryBtn" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBlueBrush}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontFamily" Value="Segoe UI, Microsoft YaHei"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Padding" Value="22,0"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" CornerRadius="8" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Bd" Property="Background" Value="#2563eb"/></Trigger>
<Trigger Property="IsPressed" Value="True"><Setter TargetName="Bd" Property="Background" Value="#1d4ed8"/></Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#cbd5e1"/>
<Setter Property="Foreground" Value="#94a3b8"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Stop Button -->
<Style x:Key="StopBtn" TargetType="Button" BasedOn="{StaticResource PrimaryBtn}">
<Setter Property="Background" Value="{StaticResource AccentRedBrush}"/>
<Style.Triggers><Trigger Property="IsEnabled" Value="False"><Setter Property="Background" Value="#fecaca"/></Trigger></Style.Triggers>
</Style>
<!-- Browse Button -->
<Style x:Key="BrowseBtn" TargetType="Button">
<Setter Property="Background" Value="{StaticResource BgInputBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/>
<Setter Property="FontFamily" Value="Segoe UI, Microsoft YaHei"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Height" Value="34"/>
<Setter Property="Width" Value="80"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" CornerRadius="6" Background="{TemplateBinding Background}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#e2e8f0"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBlueBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<!-- WINDOW CHROME -->
<Border CornerRadius="12" Background="{StaticResource BgRootBrush}" BorderBrush="#d0d5dd" BorderThickness="1">
<Border.Effect>
<DropShadowEffect BlurRadius="20" ShadowDepth="2" Opacity="0.10" Color="#475569"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions><RowDefinition Height="38"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<!-- Title Bar -->
<Border Grid.Row="0" Background="Transparent" MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="Auto"/><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="14,0,0,0" VerticalAlignment="Center">
<Ellipse Width="9" Height="9" Fill="{StaticResource AccentBlueBrush}" Margin="0,0,8,0"/>
<TextBlock Text="MemScan-EDR" Foreground="{StaticResource TextSecondaryBrush}" FontSize="11.5" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="v1.0" HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{StaticResource TextDimBrush}" FontSize="11"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="0,0,4,0">
<Button x:Name="BtnLang" Content="中" Width="32" Height="24" Click="BtnLangToggle_Click"
Foreground="{StaticResource TextSecondaryBrush}" Background="Transparent"
BorderThickness="0" Cursor="Hand" FontSize="11" FontWeight="SemiBold"/>
<Button Content="—" Width="32" Height="24" Click="BtnMinimize"
Foreground="{StaticResource TextSecondaryBrush}" Background="Transparent"
BorderThickness="0" Cursor="Hand" FontSize="12"/>
<Button Content="□" Width="32" Height="24" Click="BtnMaximize"
Foreground="{StaticResource TextSecondaryBrush}" Background="Transparent"
BorderThickness="0" Cursor="Hand" FontSize="13"/>
<Button Content="✕" Width="32" Height="24" Click="BtnClose"
Foreground="{StaticResource TextSecondaryBrush}" Background="Transparent"
BorderThickness="0" Cursor="Hand" FontSize="11"/>
</StackPanel>
</Grid>
</Border>
<!-- MAIN CONTENT -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions><ColumnDefinition Width="220"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<!-- SIDEBAR -->
<Border Grid.Column="0" Background="{StaticResource BgSidebarBrush}" CornerRadius="0,0,0,12">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="20,18,20,6">
<StackPanel Orientation="Horizontal">
<Border Width="34" Height="34" CornerRadius="10" Background="#334155">
<TextBlock Text="S" FontSize="17" FontWeight="Bold" Foreground="{StaticResource AccentBlueBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel Margin="10,0,0,0" VerticalAlignment="Center">
<TextBlock Text="MemScan" FontSize="17" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="EDR v1.0" FontSize="11" Foreground="{StaticResource SidebarTextDimBrush}"/>
</StackPanel>
</StackPanel>
</StackPanel>
<StackPanel Grid.Row="1" Margin="0,14,0,0">
<TextBlock x:Name="TbMonHeader" Text="监控" Margin="24,8,0,6" FontSize="10"
Foreground="{StaticResource SidebarTextDimBrush}" FontWeight="SemiBold"/>
<RadioButton x:Name="NavDashboard" Style="{StaticResource NavBtn}" IsChecked="True" Checked="NavChanged">
<StackPanel Orientation="Horizontal">
<TextBlock Text="◆" FontSize="10" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock x:Name="TbNavDashboard" Text="仪表盘" VerticalAlignment="Center"/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="NavProcess" Style="{StaticResource NavBtn}" Checked="NavChanged">
<StackPanel Orientation="Horizontal">
<TextBlock Text="◆" FontSize="10" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock x:Name="TbNavProcess" Text="进程列表" VerticalAlignment="Center"/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="NavScan" Style="{StaticResource NavBtn}" Checked="NavChanged">
<StackPanel Orientation="Horizontal">
<TextBlock Text="◆" FontSize="10" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock x:Name="TbNavScan" Text="扫描进度" VerticalAlignment="Center"/>
</StackPanel>
</RadioButton>
<TextBlock x:Name="TbSysHeader" Text="系统" Margin="24,20,0,6" FontSize="10"
Foreground="{StaticResource SidebarTextDimBrush}" FontWeight="SemiBold"/>
<RadioButton x:Name="NavSettings" Style="{StaticResource NavBtn}" Checked="NavChanged">
<StackPanel Orientation="Horizontal">
<TextBlock Text="◆" FontSize="10" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock x:Name="TbNavSettings" Text="设置" VerticalAlignment="Center"/>
</StackPanel>
</RadioButton>
</StackPanel>
<StackPanel Grid.Row="2" Margin="16,0,16,18">
<Border Background="#334155" CornerRadius="10" Padding="12,10">
<StackPanel>
<TextBlock x:Name="TbQuickScan" Text="快速扫描" FontSize="12" FontWeight="SemiBold"
Foreground="{StaticResource AccentRedBrush}"/>
<TextBlock x:Name="TbQuickScanDesc" Text="立即扫描所有进程" FontSize="10.5"
Foreground="{StaticResource SidebarTextDimBrush}" Margin="0,4,0,8"/>
<Button x:Name="BtnStartScan" Content="开始扫描" Style="{StaticResource PrimaryBtn}"
Height="34" FontSize="12" Click="BtnStartScan_Click"/>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Border>
<!-- CONTENT AREA -->
<Border Grid.Column="1" Background="{StaticResource BgContentBrush}" CornerRadius="0,0,12,0">
<Grid x:Name="ContentArea">
<!-- DASHBOARD -->
<Grid x:Name="DashboardView" Margin="24,18,24,20" Visibility="Visible">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,14">
<TextBlock x:Name="TbDashboardTitle" Text="安全仪表盘" FontSize="20" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}"/>
<Border Background="{StaticResource HighlightBlueBrush}" CornerRadius="10" Padding="10,3" Margin="14,0,0,0" VerticalAlignment="Center">
<TextBlock x:Name="LblDashboardStatus" Text="● 监控中" FontSize="10.5" FontWeight="SemiBold"
Foreground="{StaticResource AccentBlueBrush}"/>
</Border>
</StackPanel>
<Grid Grid.Row="1" Margin="0,0,0,14">
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/><ColumnDefinition Width="*"/><ColumnDefinition Width="10"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
<Border Grid.Column="0" CornerRadius="10" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" Padding="16,12">
<StackPanel>
<TextBlock x:Name="TbStatTotal" Text="已扫描" FontSize="10.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold"/>
<TextBlock x:Name="StatTotal" Text="0" FontSize="30" FontWeight="Bold" Foreground="{StaticResource AccentBlueBrush}" Margin="0,4,0,0"/>
<TextBlock x:Name="TbStatTotalDesc" Text="进程" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel>
</Border>
<Border Grid.Column="2" CornerRadius="10" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" Padding="16,12">
<StackPanel>
<TextBlock x:Name="TbStatHigh" Text="高风险" FontSize="10.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold"/>
<TextBlock x:Name="StatHigh" Text="0" FontSize="30" FontWeight="Bold" Foreground="{StaticResource AccentRedBrush}" Margin="0,4,0,0"/>
<TextBlock x:Name="TbStatHighDesc" Text="严重告警" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel>
</Border>
<Border Grid.Column="4" CornerRadius="10" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" Padding="16,12">
<StackPanel>
<TextBlock x:Name="TbStatSusp" Text="可疑" FontSize="10.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold"/>
<TextBlock x:Name="StatSuspicious" Text="0" FontSize="30" FontWeight="Bold" Foreground="{StaticResource AccentYellowBrush}" Margin="0,4,0,0"/>
<TextBlock x:Name="TbStatSuspDesc" Text="警告" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel>
</Border>
<Border Grid.Column="6" CornerRadius="10" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" Padding="16,12">
<StackPanel>
<TextBlock x:Name="TbStatSafe" Text="安全" FontSize="10.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold"/>
<TextBlock x:Name="StatSafe" Text="0" FontSize="30" FontWeight="Bold" Foreground="{StaticResource AccentGreenBrush}" Margin="0,4,0,0"/>
<TextBlock x:Name="TbStatSafeDesc" Text="正常" FontSize="11" Foreground="{StaticResource TextSecondaryBrush}"/>
</StackPanel>
</Border>
</Grid>
<Grid Grid.Row="2">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock x:Name="TbRecentAlerts" Text="最近告警" FontSize="15" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
</StackPanel>
<DataGrid x:Name="DgAlerts" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True"
HeadersVisibility="Column" Background="{StaticResource BgCardBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
Foreground="{StaticResource TextPrimaryBrush}" GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="{StaticResource BorderDimBrush}"
RowHeight="36" SelectionUnit="FullRow"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<DataGrid.Resources><Style TargetType="DataGridColumnHeader"><Setter Property="Background" Value="#f1f5f9"/><Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/><Setter Property="FontSize" Value="11"/><Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Padding" Value="10,6"/><Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}"/><Setter Property="BorderThickness" Value="0,0,0,1"/></Style></DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="RISK" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="8" Height="8" Margin="0,0,6,0" Fill="{StaticResource AccentGreenBrush}"/>
<TextBlock Text="{Binding RiskLevel}" FontSize="11.5" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="PID" Binding="{Binding Pid}" Width="55"/>
<DataGridTextColumn Header="PROCESS" Binding="{Binding ProcessName}" Width="*" MinWidth="120"/>
<DataGridTextColumn Header="SCORE" Binding="{Binding RiskScore, StringFormat={}{0:F2}}" Width="60"/>
<DataGridTextColumn Header="PROVIDER" Binding="{Binding AIProvider}" Width="130"/>
<DataGridTextColumn Header="TIME" Binding="{Binding ScanTime, StringFormat=HH:mm:ss}" Width="70"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
<!-- PROCESS LIST -->
<Grid x:Name="ProcessView" Margin="24,18,24,20" Visibility="Collapsed">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,14">
<TextBlock x:Name="TbProcTitle" Text="扫描进程" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock x:Name="LblProcCount" Text=" - 0 条结果" FontSize="13" Foreground="{StaticResource TextDimBrush}" VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<DataGrid x:Name="DgProcesses" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserSortColumns="True"
HeadersVisibility="Column" Background="{StaticResource BgCardBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
Foreground="{StaticResource TextPrimaryBrush}" GridLinesVisibility="Horizontal"
HorizontalGridLinesBrush="{StaticResource BorderDimBrush}"
RowHeight="36" SelectionUnit="FullRow">
<DataGrid.Resources><Style TargetType="DataGridColumnHeader"><Setter Property="Background" Value="#f1f5f9"/><Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}"/><Setter Property="FontSize" Value="11"/><Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Padding" Value="10,6"/><Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}"/><Setter Property="BorderThickness" Value="0,0,0,1"/></Style></DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="RISK" Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="8" Height="8" Margin="0,0,6,0" Fill="{StaticResource AccentGreenBrush}"/>
<TextBlock Text="{Binding RiskLevel}" FontSize="11.5" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="PID" Binding="{Binding Pid}" Width="55"/>
<DataGridTextColumn Header="NAME" Binding="{Binding ProcessName}" Width="130"/>
<DataGridTextColumn Header="SCORE" Binding="{Binding RiskScore, StringFormat={}{0:F2}}" Width="70"/>
<DataGridTextColumn Header="INDICATORS" Binding="{Binding Indicators, Converter={StaticResource ListToStringConv}}" Width="180"/>
<DataGridTextColumn Header="PROVIDER" Binding="{Binding AIProvider}" Width="140"/>
<DataGridTextColumn Header="TIME" Binding="{Binding ScanTime, StringFormat=yyyy-MM-dd HH:mm:ss}" Width="140"/>
<DataGridTextColumn Header="REASONING" Binding="{Binding Reasoning}" Width="*" MinWidth="200"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- SCAN PROGRESS -->
<Grid x:Name="ScanView" Margin="24,18,24,20" Visibility="Collapsed">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,14">
<TextBlock x:Name="TbScanTitle" Text="扫描进度" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
<Border x:Name="ScanStatusBadge" Background="#f0fdf4" CornerRadius="10" Padding="10,3" Margin="14,0,0,0" VerticalAlignment="Center">
<TextBlock x:Name="LblScanStatus" Text="● 空闲" FontSize="10.5" FontWeight="SemiBold" Foreground="{StaticResource AccentGreenBrush}"/>
</Border>
</StackPanel>
<ProgressBar x:Name="ScanProgressBar" Grid.Row="1" Height="8" Margin="0,0,0,10" Minimum="0" Maximum="100" Value="0" Background="#e2e8f0" Foreground="{StaticResource AccentBlueBrush}" BorderThickness="0"/>
<Border Grid.Row="2" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" CornerRadius="8" Padding="14,8" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Scanning:" FontSize="12.5" Foreground="{StaticResource TextDimBrush}" VerticalAlignment="Center"/>
<TextBlock x:Name="LblCurrentProcess" Text="-" FontSize="12.5" FontWeight="SemiBold" Margin="8,0,0,0" Foreground="{StaticResource AccentBlueBrush}" FontFamily="Consolas, Courier New" VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Button x:Name="BtnStopScan" Grid.Row="3" Content="停止扫描" Style="{StaticResource StopBtn}" Width="130" HorizontalAlignment="Left" Margin="0,0,0,10" Click="BtnStopScan_Click" IsEnabled="False"/>
<Grid Grid.Row="4">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<TextBlock x:Name="TbScanLog" Grid.Row="0" Text="扫描日志" FontSize="14" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,6"/>
<Border Grid.Row="1" Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" CornerRadius="8">
<ListBox x:Name="LstScanLog" Background="Transparent" Foreground="{StaticResource TextSecondaryBrush}" BorderThickness="0" Margin="2"
FontFamily="Consolas, Courier New" FontSize="11.5">
<ListBox.ItemTemplate><DataTemplate><TextBlock Text="{Binding}" TextWrapping="NoWrap" Padding="4,1"/></DataTemplate></ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
</Grid>
<!-- SETTINGS -->
<Grid x:Name="SettingsView" Margin="24,18,24,20" Visibility="Collapsed">
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,18">
<TextBlock x:Name="TbSettingsTitle" Text="设置" FontSize="20" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}"/>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- AI Config -->
<Border Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" CornerRadius="10" Padding="20,16" Margin="0,0,0,14">
<StackPanel>
<TextBlock x:Name="TbAiConfig" Text="AI 配置" FontSize="15" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14"/>
<TextBlock x:Name="TbAiProvider" Text="AI 提供商" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<ComboBox x:Name="CmbAIProvider" Height="34" FontSize="13" Margin="0,0,0,12"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1">
<ComboBoxItem Content="启发式规则 (无需 API)" Tag="Heuristic"/>
<ComboBoxItem Content="OpenAI 兼容 (OpenAI / DeepSeek / 通义等)" Tag="OpenAICompatible"/>
<ComboBoxItem Content="Claude (Anthropic API)" Tag="Claude"/>
<ComboBoxItem Content="本地模型 (Ollama / LM Studio 等)" Tag="Local"/>
</ComboBox>
<TextBlock x:Name="TbBaseUrl" Text="API 地址" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBox x:Name="TxtBaseUrl" Text="" FontSize="13" Height="34" Margin="0,0,0,12"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
<TextBlock x:Name="TbApiKey" Text="API 密钥" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<PasswordBox x:Name="TxtApiKey" FontSize="13" Height="34" Margin="0,0,0,12"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
<TextBlock x:Name="TbModel" Text="模型名称" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<TextBox x:Name="TxtModel" Text="" FontSize="13" Height="34"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
</StackPanel>
</Border>
<!-- Path Config -->
<Border Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" CornerRadius="10" Padding="20,16" Margin="0,0,0,14">
<StackPanel>
<TextBlock x:Name="TbPathConfig" Text="路径配置" FontSize="15" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14"/>
<TextBlock x:Name="TbCeMcpPath" Text="CE-MCP 服务器路径" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBox x:Name="TxtCeMcpPath" Text="" FontSize="13" Height="34"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
<Button Grid.Column="2" Content="浏览" Style="{StaticResource BrowseBtn}" Click="BtnBrowseCeMcp_Click"/>
</Grid>
<TextBlock x:Name="TbRulesDir" Text="扫描规则目录" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBox x:Name="TxtRulesDir" Text="" FontSize="13" Height="34"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
<Button Grid.Column="2" Content="浏览" Style="{StaticResource BrowseBtn}" Click="BtnBrowseRules_Click"/>
</Grid>
<TextBlock x:Name="TbLogDir" Text="日志目录" FontSize="11.5" Foreground="{StaticResource TextDimBrush}" FontWeight="SemiBold" Margin="0,0,0,5"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="6"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<TextBox x:Name="TxtLogDir" Text="" FontSize="13" Height="34"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
CaretBrush="{StaticResource AccentBlueBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1"
VerticalContentAlignment="Center" Padding="10,0"/>
<Button Grid.Column="2" Content="浏览" Style="{StaticResource BrowseBtn}" Click="BtnBrowseLog_Click"/>
</Grid>
</StackPanel>
</Border>
<!-- Scan Profile -->
<Border Background="{StaticResource BgCardBrush}" BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1" CornerRadius="10" Padding="20,16" Margin="0,0,0,14">
<StackPanel>
<TextBlock x:Name="TbScanProfile" Text="扫描配置" FontSize="15" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,14"/>
<ComboBox x:Name="CmbScanProfile" Height="34" FontSize="13"
Background="{StaticResource BgInputBrush}" Foreground="{StaticResource TextPrimaryBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1">
<ComboBoxItem Content="快速 - 简单扫描" Tag="fast"/>
<ComboBoxItem Content="平衡 - 默认扫描深度" Tag="balanced"/>
<ComboBoxItem Content="深度 - 完整内存 + AI 分析" Tag="deep"/>
</ComboBox>
</StackPanel>
</Border>
<Button x:Name="BtnSaveSettings" Content="保存设置" Style="{StaticResource PrimaryBtn}"
Width="160" HorizontalAlignment="Left" Margin="0,2,0,30" Click="BtnSaveSettings_Click"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</Window>

View File

@ -0,0 +1,549 @@
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;
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<AssemblyName>MemScan-EDR</AssemblyName>
<RootNamespace>MemScanEDR</RootNamespace>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,43 @@
namespace MemScanEDR.Models;
public enum AIProvider
{
Heuristic,
OpenAICompatible,
Claude,
Local
}
public class AIConfig
{
public AIProvider Provider { get; set; } = AIProvider.Heuristic;
public string ApiKey { get; set; } = "";
public string BaseUrl { get; set; } = "";
public string Model { get; set; } = "";
public int MaxTokens { get; set; } = 2000;
public double Temperature { get; set; } = 0.3;
public int Timeout { get; set; } = 30;
public static AIConfig DefaultFor(AIProvider provider) => provider switch
{
AIProvider.OpenAICompatible => new AIConfig
{
Provider = AIProvider.OpenAICompatible,
BaseUrl = "https://api.openai.com/v1",
Model = "gpt-4o-mini"
},
AIProvider.Claude => new AIConfig
{
Provider = AIProvider.Claude,
BaseUrl = "https://api.anthropic.com",
Model = "claude-3-5-sonnet-20241022"
},
AIProvider.Local => new AIConfig
{
Provider = AIProvider.Local,
BaseUrl = "http://127.0.0.1:11434",
Model = "llama3"
},
_ => new AIConfig { Provider = AIProvider.Heuristic }
};
}

View File

@ -0,0 +1,43 @@
using System.Text.Json;
using System.IO;
namespace MemScanEDR.Models;
public class AppSettings
{
public AIConfig AI { get; set; } = AIConfig.DefaultFor(AIProvider.Heuristic);
public string CeMcpPath { get; set; } = "auto";
public string CeMcpCommand { get; set; } = "python";
public string WebHost { get; set; } = "127.0.0.1";
public int WebPort { get; set; } = 8080;
public string ScanProfile { get; set; } = "balanced";
public string LogDir { get; set; } = "./logs";
public string DbPath { get; set; } = "./data/audit.db";
public string RulesDir { get; set; } = "./rules";
private static readonly string SettingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json");
public void Save()
{
var dir = Path.GetDirectoryName(SettingsPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(SettingsPath, json);
}
public static AppSettings Load()
{
if (!File.Exists(SettingsPath))
return new AppSettings();
try
{
var json = File.ReadAllText(SettingsPath);
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
catch
{
return new AppSettings();
}
}
}

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
namespace MemScanEDR.Models;
public class ProcessInfo
{
public int Pid { get; set; }
public string Name { get; set; } = "";
public string ExePath { get; set; } = "";
public int Ppid { get; set; }
public string ParentName { get; set; } = "";
public string CmdLine { get; set; } = "";
public string User { get; set; } = "";
public int ThreadCount { get; set; }
public long WorkingSetMB { get; set; }
public bool IsSigned { get; set; }
public string Signer { get; set; } = "";
public bool IsSystemPath { get; set; }
public bool IsTempPath { get; set; }
public List<NetworkConnection> Connections { get; set; } = new();
public List<MemoryRegion> MemoryRegions { get; set; } = new();
}
public class NetworkConnection
{
public string Protocol { get; set; } = "";
public string LocalAddr { get; set; } = "";
public string RemoteAddr { get; set; } = "";
public string State { get; set; } = "";
}
public class MemoryRegion
{
public ulong BaseAddress { get; set; }
public ulong Size { get; set; }
public string Protect { get; set; } = "";
public string Type { get; set; } = "";
public string State { get; set; } = "";
}
public class ScanResult
{
public int Pid { get; set; }
public string ProcessName { get; set; } = "";
public RiskLevel RiskLevel { get; set; } = RiskLevel.Safe;
public double RiskScore { get; set; }
public List<string> Indicators { get; set; } = new();
public string Reasoning { get; set; } = "";
public List<SignatureMatch> Matches { get; set; } = new();
public string AIProvider { get; set; } = "";
public DateTime ScanTime { get; set; } = DateTime.Now;
}
public enum RiskLevel
{
Safe,
Suspicious,
High
}
public class SignatureMatch
{
public string SigId { get; set; } = "";
public string Category { get; set; } = "";
public string Severity { get; set; } = "";
public string Description { get; set; } = "";
public string Address { get; set; } = "";
public string Protect { get; set; } = "";
}
public class Signature
{
public string SigId { get; set; } = "";
public string Value { get; set; } = "";
public string DataType { get; set; } = "string";
public string Category { get; set; } = "";
public string Severity { get; set; } = "medium";
public string Description { get; set; } = "";
}

View File

@ -0,0 +1,372 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MemScanEDR.Models;
namespace MemScanEDR.Services;
public class AIAnalyzer
{
private readonly HttpClient _http = new();
private AIConfig _config;
public AIAnalyzer(AIConfig config)
{
_config = config;
_http.Timeout = TimeSpan.FromSeconds(config.Timeout);
}
public void UpdateConfig(AIConfig config)
{
_config = config;
_http.Timeout = TimeSpan.FromSeconds(config.Timeout);
}
public async Task<ScanResult> AnalyzeProcessAsync(ProcessInfo proc, List<SignatureMatch> matches, List<MemoryRegion> suspiciousRegions)
{
if (_config.Provider == AIProvider.Heuristic)
return HeuristicAnalyze(proc, matches, suspiciousRegions);
// Pre-filter: skip API call if nothing suspicious at all
var preScore = QuickPreScore(proc, matches, suspiciousRegions);
if (preScore < 0.05)
{
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = RiskLevel.Safe,
RiskScore = 0.0,
Indicators = new List<string>(),
Reasoning = $"PID {proc.Pid} ({proc.Name}) | no suspicious indicators, skipped AI analysis",
AIProvider = "heuristic/pre-filter",
ScanTime = DateTime.Now
};
}
try
{
var prompt = BuildPrompt(proc, matches, suspiciousRegions);
var response = await CallAIAsync(prompt);
return ParseResponse(proc, response);
}
catch (Exception ex)
{
var result = HeuristicAnalyze(proc, matches, suspiciousRegions);
result.Reasoning += $" [AI API error: {ex.Message}]";
return result;
}
}
private static double QuickPreScore(ProcessInfo proc, List<SignatureMatch> matches, List<MemoryRegion> suspiciousRegions)
{
double s = 0;
if (!proc.IsSigned && proc.IsTempPath) s += 0.30;
var sysNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "svchost.exe", "csrss.exe", "lsass.exe", "winlogon.exe", "services.exe" };
if (sysNames.Contains(proc.Name) && !proc.IsSystemPath) s += 0.25;
var suspPorts = new HashSet<int> { 4444, 5555, 6666, 7777, 1337, 31337, 9999, 12345, 27015 };
if (proc.Connections.Any(c => { var parts = c.RemoteAddr.Split(':'); return parts.Length >= 2 && int.TryParse(parts[^1], out var p) && suspPorts.Contains(p); })) s += 0.20;
if (matches.Count > 0) s += 0.20;
if (suspiciousRegions.Any(r => r.Protect.Contains("EXECUTE_READWRITE"))) s += 0.15;
return Math.Min(s, 1.0);
}
private async Task<string> CallAIAsync(string userMessage)
{
if (_config.Provider == AIProvider.Claude)
return await CallClaudeAsync(userMessage);
// OpenAI-compatible / Local use the same OpenAI chat format
var url = _config.Provider == AIProvider.Local
? $"{_config.BaseUrl}/api/chat"
: $"{_config.BaseUrl.TrimEnd('/')}/chat/completions";
object requestBody = _config.Provider == AIProvider.Local
? new { model = _config.Model, messages = new[] { new { role = "system", content = SystemPrompt }, new { role = "user", content = userMessage } }, stream = false }
: new { model = _config.Model, messages = new[] { new { role = "system", content = SystemPrompt }, new { role = "user", content = userMessage } }, max_tokens = _config.MaxTokens, temperature = _config.Temperature };
var json = JsonSerializer.Serialize(requestBody);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(_config.ApiKey))
request.Headers.Add("Authorization", $"Bearer {_config.ApiKey}");
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseJson);
if (_config.Provider == AIProvider.Local)
return doc.RootElement.GetProperty("message").GetProperty("content").GetString() ?? "";
return doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString() ?? "";
}
private async Task<string> CallClaudeAsync(string userMessage)
{
var url = $"{_config.BaseUrl.TrimEnd('/')}/v1/messages";
var requestBody = new
{
model = _config.Model,
max_tokens = _config.MaxTokens,
system = SystemPrompt,
messages = new[] { new { role = "user", content = userMessage } }
};
var json = JsonSerializer.Serialize(requestBody);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
request.Headers.Add("x-api-key", _config.ApiKey);
request.Headers.Add("anthropic-version", "2023-06-01");
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseJson);
return doc.RootElement.GetProperty("content")[0].GetProperty("text").GetString() ?? "";
}
private ScanResult ParseResponse(ProcessInfo proc, string content)
{
content = content.Trim();
if (content.StartsWith("```"))
{
var lines = content.Split('\n');
content = string.Join('\n', lines[1..^1]);
}
try
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
var riskLevel = root.GetProperty("risk_level").GetString() switch
{
"high" => RiskLevel.High,
"suspicious" => RiskLevel.Suspicious,
_ => RiskLevel.Safe
};
var indicators = new List<string>();
if (root.TryGetProperty("indicators", out var indArr))
foreach (var ind in indArr.EnumerateArray())
indicators.Add(ind.GetString() ?? "");
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = riskLevel,
RiskScore = root.GetProperty("risk_score").GetDouble(),
Indicators = indicators,
Reasoning = root.GetProperty("reasoning").GetString() ?? "",
AIProvider = $"{_config.Provider}/{_config.Model}",
ScanTime = DateTime.Now
};
}
catch
{
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = RiskLevel.Safe,
RiskScore = 0,
Reasoning = content,
AIProvider = $"{_config.Provider}/{_config.Model}",
ScanTime = DateTime.Now
};
}
}
private string BuildPrompt(ProcessInfo proc, List<SignatureMatch> matches, List<MemoryRegion> suspiciousRegions)
{
var sb = new StringBuilder();
sb.AppendLine("=== PROCESS DATA ===");
sb.AppendLine($"PID: {proc.Pid}");
sb.AppendLine($"Name: {proc.Name}");
sb.AppendLine($"Path: {(string.IsNullOrEmpty(proc.ExePath) ? "unknown" : proc.ExePath)}");
sb.AppendLine($"Parent: {proc.ParentName} (PPID: {proc.Ppid})");
sb.AppendLine($"CommandLine: {proc.CmdLine}");
sb.AppendLine($"Signed: {proc.IsSigned}");
sb.AppendLine($"Signer: {(string.IsNullOrEmpty(proc.Signer) ? "-" : proc.Signer)}");
sb.AppendLine($"SystemPath: {proc.IsSystemPath}");
sb.AppendLine($"TempPath: {proc.IsTempPath}");
sb.AppendLine($"ThreadCount: {proc.ThreadCount}");
sb.AppendLine($"WorkingSetMB: {proc.WorkingSetMB}");
if (proc.Connections.Count > 0)
{
sb.AppendLine($"\nNetworkConnections ({proc.Connections.Count}):");
foreach (var c in proc.Connections.Take(8))
sb.AppendLine($" {c.Protocol} {c.LocalAddr} -> {c.RemoteAddr} [{c.State}]");
}
else
{
sb.AppendLine("\nNetworkConnections: none");
}
if (matches.Count > 0)
{
sb.AppendLine($"\nMemorySignatures ({matches.Count}):");
foreach (var m in matches.Take(10))
sb.AppendLine($" [{m.Severity}] [{m.Category}] {m.SigId} @ {m.Address}: {m.Description}");
}
else
{
sb.AppendLine("\nMemorySignatures: none");
}
if (suspiciousRegions.Count > 0)
{
sb.AppendLine($"\nSuspiciousMemoryRegions ({suspiciousRegions.Count}):");
foreach (var r in suspiciousRegions.Take(5))
sb.AppendLine($" 0x{r.BaseAddress:X} size={r.Size} protect={r.Protect} type={r.Type} state={r.State}");
}
else
{
sb.AppendLine("\nSuspiciousMemoryRegions: none");
}
sb.AppendLine("\n=== ANALYZE THIS PROCESS ===");
return sb.ToString();
}
public ScanResult HeuristicAnalyze(ProcessInfo proc, List<SignatureMatch> matches, List<MemoryRegion> suspiciousRegions)
{
double score = 0;
var indicators = new List<string>();
var reasons = new List<string>();
// Rule 1: unsigned + temp path
if (!proc.IsSigned && proc.IsTempPath)
{
score += 0.30;
indicators.Add("unsigned_temp_path");
reasons.Add("unsigned executable in temp directory");
}
// Rule 2: process masquerading
var sysNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
"svchost.exe", "csrss.exe", "lsass.exe", "winlogon.exe", "services.exe"
};
if (sysNames.Contains(proc.Name) && !proc.IsSystemPath)
{
score += 0.25;
indicators.Add("process_masquerading");
reasons.Add($"system process name in non-system path");
}
// Rule 3: suspicious ports
var suspPorts = new HashSet<int> { 4444, 5555, 6666, 7777, 1337, 31337 };
foreach (var conn in proc.Connections)
{
var parts = conn.RemoteAddr.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out var port) && suspPorts.Contains(port))
{
score += 0.20;
indicators.Add("suspicious_port");
reasons.Add($"suspicious remote port {port}");
break;
}
}
// Rule 4: signature matches
if (matches.Count > 0)
{
var high = matches.Count(m => m.Severity == "high");
var med = matches.Count(m => m.Severity == "medium");
score += Math.Min(high * 0.20, 0.4);
score += Math.Min(med * 0.08, 0.2);
if (high > 0) { indicators.Add("high_severity_match"); reasons.Add($"{high} high-severity signatures"); }
if (matches.Any(m => m.Category == "shellcode")) { score += 0.15; indicators.Add("shellcode_detected"); }
if (matches.Any(m => m.Category == "c2")) { score += 0.20; indicators.Add("c2_pattern"); }
}
// Rule 5: suspicious memory regions
var rwxCount = suspiciousRegions.Count(r => r.Protect.Contains("EXECUTE_READWRITE") && r.Type == "PRIVATE");
if (rwxCount > 0) { score += Math.Min(rwxCount * 0.12, 0.25); indicators.Add("rwx_private_memory"); }
score = Math.Min(score, 1.0);
var level = score >= 0.75 ? RiskLevel.High : score >= 0.50 ? RiskLevel.Suspicious : RiskLevel.Safe;
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = level,
RiskScore = score,
Indicators = indicators,
Reasoning = $"PID {proc.Pid} ({proc.Name}) | {string.Join("; ", reasons)} | score={score:F2}",
AIProvider = "heuristic/rule-engine-v1",
ScanTime = DateTime.Now
};
}
public async Task<(bool ok, string message)> TestConnectionAsync()
{
if (_config.Provider == AIProvider.Heuristic) return (true, "Heuristic mode (no API)");
try
{
await CallAIAsync("ping");
return (true, $"OK - {_config.Provider}/{_config.Model}");
}
catch (Exception ex) { return (false, ex.Message); }
}
private const string SystemPrompt = @"You are a Windows memory security expert analyzing a single process for threats.
=== YOUR TASK ===
Given process metadata, memory scan results, and memory region data, determine if the process is safe, suspicious, or high risk.
=== RISK LEVEL DEFINITIONS ===
""safe"" The process exhibits NORMAL or benign behavior:
- Signed binaries from trusted publishers (Microsoft, Adobe, major vendors)
- DLL injection by standard antivirus/EDR components
- Internet connections to normal CDNs/cloud services (Azure, AWS, Google)
- Small private RWX regions used by .NET JIT, JavaScript engines (node.js), or Java
- Hooking by regular security products
- Processes in System32 that are known Windows components
**IMPORTANT: The vast majority (>95%) of processes are SAFE. Default to ""safe"" unless you have clear threat evidence.**
""suspicious"" ONE or TWO of the following are present:
- Unsigned executable in temporary folder (%TEMP%, Downloads)
- Process with system-critical name (svchost.exe, lsass.exe, csrss.exe) running from non-system path
- Known C2 ports (4444, 5555, 6666, 1337, etc.)
- Multiple large PAGE_EXECUTE_READWRITE private regions
- Signature matches of medium severity
- Unsigned DLL loaded from suspicious path
**A process needs at least 2-3 weak signals to be suspicious, or 1 strong signal.**
""high"" CLEAR evidence of malware, requiring MULTIPLE concurring signals:
- Shellcode signature match + RWX private memory + unsigned temp path (all 3 together)
- C2 pattern match + network to suspicious remote address
- Reflective DLL injection signature + PE header in private memory
- Process hollowing or process injection signature matches
**Single indicators are NOT enough for ""high"". Requires 3+ concurring indicators.**
=== JSON RESPONSE FORMAT ===
Respond with ONLY this JSON (no markdown, no explanation):
{""risk_level"":""safe"",""risk_score"":0.0,""indicators"":[],""reasoning"":""""}
Fields:
- risk_level: ""safe"" | ""suspicious"" | ""high""
- risk_score: 0.01.0 (0.00.30=safe, 0.310.69=suspicious, 0.701.0=high)
- indicators: list of 05 key indicator strings (e.g. ""unsigned_temp_binary"", ""rwx_shellcode_region"", ""c2_network"", ""process_masquerading"", ""dll_injection_sig"")
- reasoning: 12 sentence summary
=== IMPORTANT RULES ===
1. Error on the side of ""safe"". Only elevate when you see CONCRETE threat indicators.
2. A memory signature match alone does NOT make a process malicious many legitimate tools use similar techniques.
3. .NET/Java/Node.js processes legitimately use RWX memory this alone is NOT suspicious.
4. Consider the process name and path context calc.exe in System32 is safe; calc.exe in %TEMP% is not.
5. Process injection indicators combined with unsigned temp binaries is dangerous.
6. Only list indicators that are actually present never fabricate evidence.";
}

View File

@ -0,0 +1,65 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace MemScanEDR.Services;
public class CeMcpManager
{
private Process? _process;
public bool IsRunning => _process != null && !_process.HasExited;
public string FindServerPath()
{
var candidates = new[]
{
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "src", "ce_mcp_server", "server.py"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ce_mcp_server", "server.py"),
@"d:\github\ce-mcp-server\server.py",
@"d:\github\MemScan-EDR\src\ce_mcp_server\server.py",
Path.Combine(Directory.GetCurrentDirectory(), "src", "ce_mcp_server", "server.py"),
};
foreach (var c in candidates)
if (File.Exists(c)) return c;
return "";
}
public bool Start(string pythonCommand = "python", string? serverPath = null)
{
if (IsRunning) return true;
serverPath ??= FindServerPath();
if (string.IsNullOrEmpty(serverPath) || !File.Exists(serverPath)) return false;
try
{
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = pythonCommand,
Arguments = $"\"{serverPath}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
EnableRaisingEvents = true
};
_process.Start();
return true;
}
catch { return false; }
}
public void Stop()
{
if (_process != null && !_process.HasExited)
{
try { _process.Kill(); } catch { }
_process.Dispose();
_process = null;
}
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using MemScanEDR.Models;
namespace MemScanEDR.Services;
public class MemoryScanner
{
public List<Signature> LoadSignatures(string rulesDir)
{
var sigs = GetBuiltinSignatures();
// TODO: Load YARA rules from rulesDir
return sigs;
}
public List<SignatureMatch> ScanProcess(int pid, List<Signature> signatures)
{
var matches = new List<SignatureMatch>();
var hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
if (hProcess == IntPtr.Zero) return matches;
try
{
foreach (var sig in signatures)
{
var results = ScanMemory(hProcess, sig);
matches.AddRange(results);
}
}
finally { CloseHandle(hProcess); }
return matches;
}
private List<SignatureMatch> ScanMemory(IntPtr hProcess, Signature sig)
{
var results = new List<SignatureMatch>();
var address = 0UL;
var mbi = new MEMORY_BASIC_INFORMATION();
var mbiSize = (uint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>();
while (VirtualQueryEx(hProcess, (IntPtr)address, out mbi, mbiSize) != 0)
{
if (mbi.State == MEM_COMMIT && mbi.Protect != PAGE_NOACCESS && mbi.Protect != PAGE_GUARD)
{
var regionSize = (ulong)mbi.RegionSize;
if (regionSize > 0 && regionSize < 100 * 1024 * 1024) // max 100MB per region
{
var protect = GetProtectString(mbi.Protect);
var type = GetTypeString(mbi.Type);
// Check for suspicious patterns
if (sig.Category == "shellcode" || sig.Category == "c2" || sig.Category == "malware")
{
if (protect.Contains("EXECUTE") && type == "PRIVATE")
{
results.Add(new SignatureMatch
{
SigId = sig.SigId,
Category = sig.Category,
Severity = sig.Severity,
Description = sig.Description,
Address = $"0x{address:X}",
Protect = protect
});
}
}
}
}
address = (ulong)mbi.BaseAddress + (ulong)mbi.RegionSize;
if (address <= (ulong)mbi.BaseAddress) break; // overflow protection
}
return results;
}
public List<MemoryRegion> GetMemoryRegions(int pid)
{
var regions = new List<MemoryRegion>();
var hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
if (hProcess == IntPtr.Zero) return regions;
try
{
var address = 0UL;
var mbi = new MEMORY_BASIC_INFORMATION();
var mbiSize = (uint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>();
while (VirtualQueryEx(hProcess, (IntPtr)address, out mbi, mbiSize) != 0)
{
if (mbi.State == MEM_COMMIT)
{
regions.Add(new MemoryRegion
{
BaseAddress = (ulong)mbi.BaseAddress,
Size = (ulong)mbi.RegionSize,
Protect = GetProtectString(mbi.Protect),
Type = GetTypeString(mbi.Type),
State = "COMMIT"
});
}
address = (ulong)mbi.BaseAddress + (ulong)mbi.RegionSize;
if (address <= (ulong)mbi.BaseAddress) break;
}
}
finally { CloseHandle(hProcess); }
return regions;
}
public List<MemoryRegion> GetSuspiciousRegions(int pid)
{
var regions = GetMemoryRegions(pid);
return regions.Where(r =>
(r.Protect.Contains("EXECUTE_READWRITE") && r.Type == "PRIVATE") ||
(r.Protect.Contains("EXECUTE") && r.Type != "IMAGE")
).ToList();
}
private static string GetProtectString(uint protect) => protect switch
{
0x02 => "READONLY",
0x04 => "READWRITE",
0x08 => "WRITECOPY",
0x10 => "EXECUTE",
0x20 => "EXECUTE_READ",
0x40 => "EXECUTE_READWRITE",
0x80 => "EXECUTE_WRITECOPY",
_ => $"0x{protect:X2}"
};
private static string GetTypeString(uint type) => type switch
{
0x20000 => "PRIVATE",
0x40000 => "MAPPED",
0x1000000 => "IMAGE",
_ => $"0x{type:X}"
};
public static List<Signature> GetBuiltinSignatures() => new()
{
new() { SigId = "SHELLCODE_NOP_SLED", Value = "\x90\x90\x90\x90", DataType = "string", Category = "shellcode", Severity = "medium", Description = "NOP sled detected" },
new() { SigId = "SHELLCODE_GETEIP", Value = "\xE8\x00\x00\x00\x00\x58", DataType = "string", Category = "shellcode", Severity = "medium", Description = "CALL/POP EIP technique" },
new() { SigId = "SHELLCODE_ROR13", Value = "\xC1\xC8\x0D", DataType = "string", Category = "shellcode", Severity = "medium", Description = "ROR13 API hash pattern" },
new() { SigId = "C2_COBALTSTRIKE", Value = "beacon", DataType = "string", Category = "c2", Severity = "high", Description = "CobaltStrike Beacon string" },
new() { SigId = "C2_METERPRETER", Value = "WS2_32.dll", DataType = "string", Category = "c2", Severity = "medium", Description = "Meterpreter WinSock" },
new() { SigId = "INJECTION_REFLECTIVE", Value = "ReflectiveLoader", DataType = "string", Category = "injection", Severity = "high", Description = "Reflective DLL injection" },
new() { SigId = "INJECTION_CRT", Value = "CreateRemoteThread", DataType = "string", Category = "injection", Severity = "medium", Description = "Remote thread injection" },
new() { SigId = "INJECTION_VAE", Value = "VirtualAllocEx", DataType = "string", Category = "injection", Severity = "medium", Description = "VirtualAllocEx injection" },
new() { SigId = "MALWARE_MIMIKATZ", Value = "mimikatz", DataType = "string", Category = "malware", Severity = "high", Description = "Mimikatz credential theft" },
new() { SigId = "CRYPTO_MINER", Value = "stratum+tcp://", DataType = "string", Category = "crypto", Severity = "high", Description = "Crypto mining pool" },
new() { SigId = "RANSOM_NOTE", Value = "YOUR_FILES_ARE_ENCRYPTED", DataType = "string", Category = "malware", Severity = "high", Description = "Ransomware note" },
new() { SigId = "PS_DOWNLOAD", Value = "IEX (New-Object", DataType = "string", Category = "malware", Severity = "medium", Description = "PowerShell download exec" },
};
private const uint PROCESS_VM_READ = 0x0010;
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
private const uint MEM_COMMIT = 0x1000;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_GUARD = 0x100;
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public IntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
}

View File

@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using MemScanEDR.Models;
namespace MemScanEDR.Services;
public class ProcessCollector
{
private static readonly string[] SystemPaths = {
@"C:\Windows\System32", @"C:\Windows\SysWOW64", @"C:\Windows"
};
private static readonly HashSet<string> SystemNames = new(StringComparer.OrdinalIgnoreCase) {
"svchost.exe", "csrss.exe", "winlogon.exe", "services.exe",
"lsass.exe", "spoolsv.exe", "smss.exe", "wininit.exe",
"dwm.exe", "taskhostw.exe", "sihost.exe", "runtimebroker.exe",
"explorer.exe", "system", "registry"
};
public List<ProcessInfo> CollectAll()
{
var processes = new List<ProcessInfo>();
var connections = GetConnectionsByPid();
foreach (var proc in Process.GetProcesses())
{
try
{
var info = new ProcessInfo
{
Pid = proc.Id,
Name = proc.ProcessName,
ThreadCount = proc.Threads.Count,
WorkingSetMB = proc.WorkingSet64 / (1024 * 1024)
};
try { info.ExePath = proc.MainModule?.FileName ?? ""; } catch { }
try { info.Ppid = GetParentPid(proc.Id); } catch { }
if (info.Ppid > 0)
{
try { info.ParentName = Process.GetProcessById(info.Ppid).ProcessName; }
catch { info.ParentName = "unknown"; }
}
try { info.CmdLine = GetCommandLine(proc.Id); } catch { }
var exeLower = info.ExePath.ToLowerInvariant();
info.IsSystemPath = SystemPaths.Any(p => exeLower.StartsWith(p.ToLowerInvariant()));
info.IsTempPath = exeLower.Contains("\\temp\\") || exeLower.Contains("\\tmp\\") ||
exeLower.Contains("\\appdata\\local\\temp\\");
info.IsSigned = info.IsSystemPath;
info.Signer = info.IsSystemPath ? "Microsoft Windows" : "";
if (connections.TryGetValue(proc.Id, out var conns))
info.Connections = conns;
processes.Add(info);
}
catch { }
finally { proc.Dispose(); }
}
return processes;
}
private static int GetParentPid(int pid)
{
var handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (handle == IntPtr.Zero) return 0;
try
{
var entry = new PROCESSENTRY32 { dwSize = (uint)Marshal.SizeOf<PROCESSENTRY32>() };
if (Process32First(handle, ref entry))
{
do
{
if (entry.th32ProcessID == (uint)pid)
return (int)entry.th32ParentProcessID;
} while (Process32Next(handle, ref entry));
}
return 0;
}
finally { CloseHandle(handle); }
}
private static string GetCommandLine(int pid)
{
try
{
using var searcher = new System.Management.ManagementObjectSearcher(
$"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {pid}");
foreach (System.Management.ManagementObject obj in searcher.Get())
return obj["CommandLine"]?.ToString() ?? "";
}
catch { }
return "";
}
private static Dictionary<int, List<NetworkConnection>> GetConnectionsByPid()
{
var result = new Dictionary<int, List<NetworkConnection>>();
try
{
var properties = IPGlobalProperties.GetIPGlobalProperties();
foreach (var conn in properties.GetActiveTcpConnections())
{
var pid = GetConnectionPid(conn);
if (pid > 0)
{
if (!result.ContainsKey(pid)) result[pid] = new();
result[pid].Add(new NetworkConnection
{
Protocol = "tcp",
LocalAddr = conn.LocalEndPoint.ToString(),
RemoteAddr = conn.RemoteEndPoint.ToString(),
State = conn.State.ToString()
});
}
}
}
catch { }
return result;
}
private static int GetConnectionPid(TcpConnectionInformation conn) => 0; // simplified
private const uint TH32CS_SNAPPROCESS = 0x00000002;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct PROCESSENTRY32
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll")]
private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
[DllImport("kernel32.dll")]
private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hObject);
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>