feat: A+B 缺口全补 + README 待办记录
A 结构层小缺口 (3/3): ✅ structure.py 加 LongFilename 告警 (>120 字符成员名) ✅ extractor.py _SAFE_NAME_RE 扩展 emoji 范围 (保留 emoji 文件名不崩) ✅ 重生成 L0-023_5layer_nested.zip (5 层真嵌套) B 白名单机制 (2/2): ✅ policy.yaml 加 keyword_whitelist (内网 IP/域名自动放行 RemoteDownloadUnix/PowerShell) ✅ policy.yaml 加 yara_hash_whitelist (MD5 在白名单跳过 YARA 告警) ✅ policy.py Property 类加两个 property 读取白名单 ✅ policy.py PolicyEngine.decide() 步骤 -1: 白名单过滤 (优先级最高) ✅ scanner.py decide() 调用补传 md5 外部资源缺口 (3 条,README 已记录): - L1-009/010 需真实签名 .cvd 文件 - L1-014 需压测脚本 stress_test.py 测试靶场: ✅ rules/test_rules.yar (7 条自动生成 YARA 规则) ✅ README 新增测试靶场待办 + 白名单机制说明 + 脚本清单
This commit is contained in:
26
README.md
26
README.md
@@ -224,4 +224,30 @@ MeshSafe 会**分析宏的实际行为**:
|
||||
|
||||
---
|
||||
|
||||
## 测试靶场(待办)
|
||||
|
||||
CSV 用例集共 126 条,引擎能力覆盖约 97%。以下 3 条因需要外部资源暂搁置,待办环境就绪后补全:
|
||||
|
||||
| CSV 用例 | 内容 | 缺失原因 | 后续 |
|
||||
|----------|------|---------|------|
|
||||
| **L1-009** | ClamAV 离线病毒库热加载 | 需要真实签名的 `.cvd` 文件(ClamAV 官方下载) | 从 https://www.clamav.net/downloads 下载 daily.cvd,放入 `data/clamav-db/` 测试 |
|
||||
| **L1-010** | ClamAV 损坏库容错降级 | 需要损坏的真实 `.cvd`(自制损坏文件可能导致 clamd 无法启动) | 用 `sigtool --unpack` 解包后篡改一个字节,再重新打包测试 |
|
||||
| **L1-014** | 并发多 EICAR 压测 | 不是引擎功能,需要压测框架(并发 httpx 提交 + 监控引擎内存/耗时) | 写 `scripts/stress_test.py`,用 asyncio.Semaphore 控制并发度 |
|
||||
|
||||
### 白名单机制(v0.2.1 新增)
|
||||
|
||||
- **关键词白名单** (`policy.yaml → keyword_whitelist`):规则命中后若 detail 包含内网 IP/域名,自动放行
|
||||
- **YARA 哈希白名单** (`policy.yaml → yara_hash_whitelist`):YARA 命中后 MD5 在白名单 → 跳过告警
|
||||
- 生产部署时按需补充内网地址段 / 已知安全工具哈希
|
||||
|
||||
### 靶场样本生成脚本
|
||||
|
||||
| 脚本 | 用途 |
|
||||
|------|------|
|
||||
| `scripts/generate_csv_samples.py` | 从 CSV 批量生成 CSV 对齐样本 + index.json |
|
||||
| `scripts/generate_yara_rules.py` | 自动生成 CSV L4 段对应的 YARA `.yar` 规则 |
|
||||
| `rules/test_rules.yar` | 生成的规则文件(引擎按 mtime 自动热加载) |
|
||||
|
||||
---
|
||||
|
||||
*更多技术细节请参阅 [总体方案](docs/00-总体方案.md) 和 [接口文档](docs/接口文档.md)*
|
||||
|
||||
@@ -27,7 +27,14 @@ from app.engines.base import Finding, ScanTarget, Severity
|
||||
ARCHIVE_EXTS = {".zip", ".jar", ".war", ".apk", ".epub", ".docx", ".xlsx", ".pptx", ".odt", ".ods", ".odp"}
|
||||
TAR_EXTS = {".tar", ".gz", ".tgz", ".bz2", ".xz", ".tbz2", ".txz"}
|
||||
|
||||
_SAFE_NAME_RE = re.compile(r"[^A-Za-z0-9._一-鿿-]+")
|
||||
_SAFE_NAME_RE = re.compile(
|
||||
r"[^A-Za-z0-9._一-鿿" # ASCII + CJK 统一汉字
|
||||
r"\U0001F300-\U0001FAFF" # Emoji 表情符号
|
||||
r"\U0001F000-\U0001F2FF" # 符号和象形文字扩展 A
|
||||
r"\U00002600-\U000027BF" # 杂项符号 + 装饰符号
|
||||
r"\U00002B00-\U00002BFF" # 箭头等
|
||||
r"-]+"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -869,6 +869,7 @@ class StructureEngine(Engine):
|
||||
total_uncompressed = 0
|
||||
executable_members: List[str] = []
|
||||
traversal_members: List[str] = []
|
||||
long_filename_members: List[str] = []
|
||||
member_count = 0
|
||||
encrypted_count = 0
|
||||
|
||||
@@ -886,6 +887,8 @@ class StructureEngine(Engine):
|
||||
traversal_members.append(name)
|
||||
if info.flag_bits & 0x1: # ZIP 加密标志(bit 0)
|
||||
encrypted_count += 1
|
||||
if len(os.path.basename(name)) > 120:
|
||||
long_filename_members.append(name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return [
|
||||
Finding(
|
||||
@@ -970,6 +973,18 @@ class StructureEngine(Engine):
|
||||
)
|
||||
)
|
||||
|
||||
if long_filename_members:
|
||||
findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=Severity.LOW.value,
|
||||
rule="LongFilename",
|
||||
detail=f"归档内存在超长文件名(>120 字符){len(long_filename_members)} 个,可能规避扫描或探测边界",
|
||||
weight=5.0,
|
||||
)
|
||||
)
|
||||
|
||||
return findings
|
||||
|
||||
def _inspect_tar(self, target: ScanTarget, max_ratio: int, max_total: int) -> List[Finding]:
|
||||
|
||||
@@ -99,6 +99,17 @@ class Policy:
|
||||
"""内网特征词表规则(L2B 引擎使用)。"""
|
||||
return self.raw.get("keyword_rules", []) or []
|
||||
|
||||
@property
|
||||
def keyword_whitelist(self) -> List[Dict[str, Any]]:
|
||||
"""关键词白名单。规则命中后,若 detail 包含 allowed_keywords 中任一子串 → 过滤。"""
|
||||
return self.raw.get("keyword_whitelist", []) or []
|
||||
|
||||
@property
|
||||
def yara_hash_whitelist_set(self) -> set[str]:
|
||||
"""YARA 哈希白名单(返回 MD5 集合,便于快速查询)。"""
|
||||
raw = self.raw.get("yara_hash_whitelist", []) or []
|
||||
return {str(entry.get("md5", "")).lower() for entry in raw if isinstance(entry, dict) and entry.get("md5")}
|
||||
|
||||
@property
|
||||
def yara_weights(self) -> Dict[str, float]:
|
||||
"""YARA 规则权重映射:规则名 → weight(0-100)。
|
||||
@@ -387,6 +398,7 @@ class PolicyEngine:
|
||||
real_type: str = "",
|
||||
engine_errors: Optional[List[str]] = None,
|
||||
encrypted: bool = False,
|
||||
md5: str = "",
|
||||
) -> Decision:
|
||||
"""综合所有信号产出最终裁决。
|
||||
|
||||
@@ -410,6 +422,38 @@ class PolicyEngine:
|
||||
|
||||
ext = (declared_ext or "").lower()
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 -1:白名单过滤(优先级最高)
|
||||
# ======================================================================
|
||||
# 命中白名单的规则从 findings 中移除,不再参与任何后续裁决。
|
||||
# 包括:关键词白名单(按 rule + detail 子串匹配)+ YARA 哈希白名单。
|
||||
# ======================================================================
|
||||
wl_filtered: List[Finding] = []
|
||||
keyword_wl = policy.keyword_whitelist
|
||||
for f in findings:
|
||||
skip = False
|
||||
# 关键词白名单:detail 包含 allowed_keywords 任一子串
|
||||
if keyword_wl and f.engine == "keywords":
|
||||
for wl_entry in keyword_wl:
|
||||
if f.rule == wl_entry.get("rule"):
|
||||
allowed = wl_entry.get("allowed_keywords", [])
|
||||
if allowed and any(kw in f.detail for kw in allowed):
|
||||
skip = True
|
||||
logger.info(
|
||||
"白名单放行:rule=%s 命中关键词白名单 detail=%s",
|
||||
f.rule, f.detail,
|
||||
)
|
||||
break
|
||||
# YARA 哈希白名单:md5 在白名单集合中
|
||||
if not skip and f.engine == "yara":
|
||||
wl_set = policy.yara_hash_whitelist_set
|
||||
if wl_set and md5 and md5.lower() in wl_set:
|
||||
skip = True
|
||||
logger.info("白名单放行:YARA 命中但 MD5 在哈希白名单")
|
||||
if not skip:
|
||||
wl_filtered.append(f)
|
||||
findings = wl_filtered
|
||||
|
||||
# ======================================================================
|
||||
# 步骤 0:零容忍规则优先检查
|
||||
# ======================================================================
|
||||
|
||||
@@ -453,6 +453,7 @@ class Scanner:
|
||||
real_type=real_type,
|
||||
engine_errors=engine_errors,
|
||||
encrypted=encrypted,
|
||||
md5=root_info.get("md5", ""),
|
||||
)
|
||||
|
||||
# 计算 open_risk(独立于裁决结果,供人阅读)
|
||||
|
||||
@@ -398,6 +398,37 @@ keyword_rules:
|
||||
weight: 30
|
||||
severity: high
|
||||
|
||||
# ---------- 白名单(规则命中后二次放宽) ----------
|
||||
# 优先级:白名单 > 关键词规则 > YARA 规则 > 评分累加
|
||||
# 命中白名单的规则会从 findings 中过滤,不再进入裁决
|
||||
|
||||
keyword_whitelist:
|
||||
# ---- 内网合法运维地址/域名(白名单放行 curl/wget 下载)----
|
||||
- rule: RemoteDownloadUnix # curl/wget 下载特征
|
||||
allowed_keywords:
|
||||
- "192.168."
|
||||
- "10."
|
||||
- "172.16."
|
||||
- "172.17."
|
||||
- "172.18."
|
||||
- "172.19."
|
||||
- "172.2"
|
||||
- "172.3"
|
||||
- ".senmesh.com"
|
||||
- "repo.internal"
|
||||
- "mirror.internal"
|
||||
- rule: RemoteDownloadPowerShell # Invoke-WebRequest/BitsAdmin
|
||||
allowed_keywords:
|
||||
- "192.168."
|
||||
- "10."
|
||||
- ".senmesh.com"
|
||||
|
||||
yara_hash_whitelist:
|
||||
# YARA 命中后,文件 MD5 在此列表 → 跳过告警
|
||||
# 典型场景:已知安全的开源工具被 YARA 误匹配
|
||||
- md5: "00000000000000000000000000000000"
|
||||
note: "示例:占位,生产按需补充"
|
||||
|
||||
# ---------- 审计 ----------
|
||||
audit:
|
||||
# 记录扫描结果明细到审计日志
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user