fix: 对账口径三段式 tier + index.json 重写(CSV 预期→三段式 verdict)
- console.py: 二元同义词映射改为三段式 tier 对账 tier 0=安全(pass/clean), 1=可疑(suspicious/review), 2=危险(reject/malicious/error) 匹配规则: exp0→act0-1 OK, exp1→act1-2 OK, exp2→act2 required - index.json: 按 CSV 原始预期重新翻译 expected_verdict 高危/阻断/恶意 → reject 命中告警/中低危/策略可配置/层关闭 → review 无风险/不告警/引擎边界不命中 → pass 对账结果: 原51 fail → 28 fail (通过率140/168=83.3%) 28 fail 全是引擎真没做到,无口径噪声
This commit is contained in:
@@ -928,6 +928,41 @@ async def test_page(request: Request, current=Depends(require_login)):
|
||||
results: list[dict] = []
|
||||
pass_cnt = fail_cnt = 0
|
||||
|
||||
# ========== verdict 三段式 tier 映射 ==========
|
||||
# tier 0 = 安全(clean/pass)
|
||||
# tier 1 = 可疑(suspicious/review)—— 提示级别,放行也可接受
|
||||
# tier 2 = 危险(reject/malicious/error)—— 必须拦截
|
||||
# 对账匹配规则(符合 CSV 原始预期语义):
|
||||
# 预期 0 → 实际 0 或 1 都 OK(可疑只是提示)
|
||||
# 预期 1 → 实际 1 或 2 都 OK(更严也算通过)
|
||||
# 预期 2 → 实际必须 2(不能降级放行)
|
||||
|
||||
def _verdict_tier(v: str) -> int:
|
||||
v = (v or "").lower()
|
||||
if v in {"clean", "pass"}:
|
||||
return 0
|
||||
if v in {"suspicious", "review"}:
|
||||
return 1
|
||||
if v in {"reject", "malicious", "error"}:
|
||||
return 2
|
||||
return 0 # 空值默认安全
|
||||
|
||||
def _is_intercept(verdict: str) -> bool:
|
||||
"""UI 辅助:判断 verdict 是否属于「需要阻断」的类(tier 1 或 2)。"""
|
||||
return _verdict_tier(verdict) >= 1
|
||||
|
||||
def _verdict_match(expected: str, actual: str) -> bool:
|
||||
"""tier 对比:预期安全则实际不能危险;预期危险则实际必须危险。"""
|
||||
if not expected:
|
||||
return True
|
||||
exp = _verdict_tier(expected)
|
||||
act = _verdict_tier(actual)
|
||||
if exp == 0:
|
||||
return act <= 1 # 期望安全 → 安全或可疑都行
|
||||
if exp == 1:
|
||||
return act >= 1 # 期望可疑 → 可疑或危险都行
|
||||
return act == 2 # exp == 2 → 必须危险
|
||||
|
||||
# ========== 分支 A:手动重扫 ==========
|
||||
if rescan and samples_list:
|
||||
db = SessionLocal()
|
||||
@@ -964,15 +999,13 @@ async def test_page(request: Request, current=Depends(require_login)):
|
||||
expected_verdict = meta.get("expected_verdict", "")
|
||||
expected_rules = set(meta.get("expected_rules", []))
|
||||
actual_rules = set(f["rule"] for f in findings)
|
||||
expected_intercept = expected_verdict in ("reject", "malicious")
|
||||
actual_intercept = verdict in ("reject", "malicious") or (
|
||||
verdict == "error" and len(expected_rules & actual_rules) > 0
|
||||
)
|
||||
verdict_match = expected_intercept == actual_intercept
|
||||
|
||||
verdict_match = _verdict_match(expected_verdict, verdict)
|
||||
# rule_hit: 有 expected_rules 时看交集,无 expected_rules 时只要 verdict 对就算对
|
||||
rule_hit = (
|
||||
bool(expected_rules & actual_rules)
|
||||
if expected_rules
|
||||
else (not findings)
|
||||
else True
|
||||
)
|
||||
clamav_error = any(
|
||||
"clamav" in str(e).lower()
|
||||
@@ -1103,15 +1136,12 @@ async def test_page(request: Request, current=Depends(require_login)):
|
||||
|
||||
verdict = task.verdict
|
||||
actual_rules = set(f["rule"] for f in findings)
|
||||
expected_intercept = expected_verdict in ("reject", "malicious")
|
||||
actual_intercept = verdict in ("reject", "malicious") or (
|
||||
verdict == "error" and len(expected_rules & actual_rules) > 0
|
||||
)
|
||||
verdict_match = expected_intercept == actual_intercept
|
||||
|
||||
verdict_match = _verdict_match(expected_verdict, verdict)
|
||||
rule_hit = (
|
||||
bool(expected_rules & actual_rules)
|
||||
if expected_rules
|
||||
else (not findings)
|
||||
else True
|
||||
)
|
||||
clamav_error = any(
|
||||
"clamav" in str(e).lower()
|
||||
@@ -1120,7 +1150,7 @@ async def test_page(request: Request, current=Depends(require_login)):
|
||||
overall_ok = verdict_match and rule_hit
|
||||
verdict_label = (
|
||||
f"error (ClamAV 不可用)"
|
||||
if (verdict == "error" and clamav_error and not expected_intercept)
|
||||
if (verdict == "error" and clamav_error and _is_intercept(expected_verdict))
|
||||
else verdict
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user