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:
2026-10-29 14:40:49 +08:00
parent 6e3c1a40e5
commit 57d89b3d05
7 changed files with 125 additions and 1 deletions

View File

@@ -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

View File

@@ -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]:

View File

@@ -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:零容忍规则优先检查
# ======================================================================

View File

@@ -453,6 +453,7 @@ class Scanner:
real_type=real_type,
engine_errors=engine_errors,
encrypted=encrypted,
md5=root_info.get("md5", ""),
)
# 计算 open_risk(独立于裁决结果,供人阅读)