v1.2: Clean release with rewritten README, Chinese/English UI, AI anti-false-positive, CE-MCP auto-start
This commit is contained in:
commit
601773d487
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
201
README.md
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
# MemScan-EDR
|
||||||
|
|
||||||
|
> **Open-Source, Lightweight, Memory-Focused Windows Endpoint Detection & Response (EDR) Tool**
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://dotnet.microsoft.com/)
|
||||||
|
[](https://www.microsoft.com/windows)
|
||||||
|
[](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
89
rules/injection.yar
Normal 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
119
rules/malware_strings.yar
Normal 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
121
rules/shellcode.yar
Normal 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
61
scripts/build_csharp.bat
Normal 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
22
src-csharp/MemScanEDR.sln
Normal 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
|
||||||
5
src-csharp/MemScanEDR/App.xaml
Normal file
5
src-csharp/MemScanEDR/App.xaml
Normal 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>
|
||||||
55
src-csharp/MemScanEDR/App.xaml.cs
Normal file
55
src-csharp/MemScanEDR/App.xaml.cs
Normal 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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
489
src-csharp/MemScanEDR/MainWindow.xaml
Normal file
489
src-csharp/MemScanEDR/MainWindow.xaml
Normal 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 & 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>
|
||||||
549
src-csharp/MemScanEDR/MainWindow.xaml.cs
Normal file
549
src-csharp/MemScanEDR/MainWindow.xaml.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src-csharp/MemScanEDR/MemScanEDR.csproj
Normal file
24
src-csharp/MemScanEDR/MemScanEDR.csproj
Normal 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>
|
||||||
43
src-csharp/MemScanEDR/Models/AIConfig.cs
Normal file
43
src-csharp/MemScanEDR/Models/AIConfig.cs
Normal 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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
43
src-csharp/MemScanEDR/Models/AppSettings.cs
Normal file
43
src-csharp/MemScanEDR/Models/AppSettings.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src-csharp/MemScanEDR/Models/ProcessInfo.cs
Normal file
80
src-csharp/MemScanEDR/Models/ProcessInfo.cs
Normal 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; } = "";
|
||||||
|
}
|
||||||
372
src-csharp/MemScanEDR/Services/AIAnalyzer.cs
Normal file
372
src-csharp/MemScanEDR/Services/AIAnalyzer.cs
Normal 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.0–1.0 (0.0–0.30=safe, 0.31–0.69=suspicious, 0.70–1.0=high)
|
||||||
|
- indicators: list of 0–5 key indicator strings (e.g. ""unsigned_temp_binary"", ""rwx_shellcode_region"", ""c2_network"", ""process_masquerading"", ""dll_injection_sig"")
|
||||||
|
- reasoning: 1–2 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.";
|
||||||
|
}
|
||||||
65
src-csharp/MemScanEDR/Services/CeMcpManager.cs
Normal file
65
src-csharp/MemScanEDR/Services/CeMcpManager.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src-csharp/MemScanEDR/Services/MemoryScanner.cs
Normal file
180
src-csharp/MemScanEDR/Services/MemoryScanner.cs
Normal 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);
|
||||||
|
}
|
||||||
161
src-csharp/MemScanEDR/Services/ProcessCollector.cs
Normal file
161
src-csharp/MemScanEDR/Services/ProcessCollector.cs
Normal 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);
|
||||||
|
}
|
||||||
10
src-csharp/MemScanEDR/app.manifest
Normal file
10
src-csharp/MemScanEDR/app.manifest
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user