diff --git a/app/console.py b/app/console.py index 6c19cf9..f8d342a 100644 --- a/app/console.py +++ b/app/console.py @@ -174,7 +174,7 @@ def _annotate_yaml(raw: str) -> str: # ---- rule_overrides ---- "rule_overrides": '特定规则的处置覆盖。action: pass | manual_review | reject', # ---- yara ---- - "yara_rule_weights": 'YARA 规则按严重度分档赋权重(critical=85 high=65 medium=45 low=25)', + "yara_rule_weights": 'YARA 规则按严重度分档赋权重(critical=85 high=60 medium=30 low=10)', # ---- keyword ---- "keyword_rules": '词表引擎规则。match: contains | regex | any_of', # ---- notification ---- @@ -215,9 +215,9 @@ def _annotate_yaml(raw: str) -> str: # YARA 权重分档 → 中文 _WEIGHT_TIER_HINTS = { "critical": "weight=85(严重)", - "high": "weight=65(高危)", - "medium": "weight=45(中等)", - "low": "weight=25(低危)", + "high": "weight=60(高危)", + "medium": "weight=30(中等)", + "low": "weight=10(低危)", } lines = raw.split("\n") diff --git a/app/services/policy.py b/app/services/policy.py index b919baa..5564e12 100644 --- a/app/services/policy.py +++ b/app/services/policy.py @@ -115,13 +115,14 @@ class Policy: """YARA 规则权重映射:规则名 → weight(0-100)。 从 policy.yaml 的 yara_rule_weights.{critical/high/medium/low} 展开。 - 档位权重固定:critical=85 / high=65 / medium=45 / low=25。 + 档位权重固定:critical=85 / high=60 / medium=30 / low=10 + (与 rules/*.yar 的 meta.risk_level 分档、binary.py 默认权重一致)。 """ weights = { "critical": 85.0, - "high": 65.0, - "medium": 45.0, - "low": 25.0, + "high": 60.0, + "medium": 30.0, + "low": 10.0, } result: Dict[str, float] = {} for tier, weight in weights.items(): @@ -414,6 +415,8 @@ class PolicyEngine: 5) 规则覆盖 6) 权重累加评分 7) 阈值裁决 + 8) 复核区间上界校正 + 9) 复核地板(步骤 5 声明 manual_review 的规则,只抬处置不抬分数) """ import logging @@ -566,14 +569,15 @@ class PolicyEngine: ) # 5) 规则覆盖优先处理 - override_findings: List[Finding] = [] + # action=reject 立即拒绝;action=manual_review 只登记"复核地板", + # 不往分数里注 65 —— 地板在步骤 9 生效。 + review_floor = False for finding in findings: override = policy.rule_overrides.get(finding.rule) if not override: continue action = override.get("action", "manual_review") reason = override.get("reason") or f"规则 {finding.rule} 命中策略" - override_findings.append(finding) triggered.append( { "type": "rule_override", @@ -593,12 +597,10 @@ class PolicyEngine: triggered=triggered, ) if action == "manual_review": - score = max(score, 65.0) + review_floor = True - # 6) 其余信号按权重累加 + # 6) 全部信号按权重累加 for finding in findings: - if finding in override_findings: - continue score += finding.weight if finding.severity in {Severity.HIGH.value, Severity.CRITICAL.value}: reasons.append(f"{finding.engine}:{finding.detail}") @@ -622,6 +624,12 @@ class PolicyEngine: if score >= policy.block_score: verdict, action = "malicious", "reject" + # 9) 复核地板:策略里声明 manual_review 的规则保证"不低于人工复核", + # 与权重数值和 review_score 阈值解耦——调高 review_score 下界或调低 + # 这些规则的权重,都不会让它们覆盖的文件类型静默放行。 + if review_floor and action in ("pass", "pass_with_flag"): + verdict, action = "suspicious", "manual_review" + reason = self._build_reason(verdict, reasons, score) return Decision( verdict=verdict, @@ -753,7 +761,9 @@ class PolicyEngine: if any("url" in f.rule.lower() or "link" in f.rule.lower() or "qr" in f.rule.lower() for f in findings): factors.append("external_url") - if ext in {".exe", ".dll", ".elf", ".so", ".desktop", ".appimage", ".run", ".bin", ".deb", ".rpm"}: + if ext in {".exe", ".dll", ".elf", ".so", ".desktop", ".appimage", ".run", ".bin", ".deb", ".rpm", + ".msi", ".msp", ".com", ".pif", ".scr", ".cpl", ".jar", ".war", + ".vbs", ".vbe", ".js", ".jse", ".ps1", ".bat", ".cmd", ".hta", ".lnk", ".cab", ".appx"}: factors.append("contains_executable") if ext == ".iso": factors.append("is_mountable_image") @@ -774,6 +784,14 @@ class PolicyEngine: advice = "文件结构正常,无特别提示" requires_macro = False + # 裁决已经是恶意/引擎不可判定时,"打开风险低"是自相矛盾的对外文案: + # 摆渡系统会把这段展示给用户,等于告诉用户"被拦下的文件可以放心打开"。 + if verdict in {"malicious", "error", "reject"} and level != "critical": + level = "critical" + factors.append("verdict_rejected") + advice = "该文件已被判定为恶意或不可判定,请勿打开,联系管理员处理" + requires_macro = False + return { "level": level, "factors": factors, diff --git a/app/templates/policy.html b/app/templates/policy.html index 7cef473..7a52629 100644 --- a/app/templates/policy.html +++ b/app/templates/policy.html @@ -172,10 +172,10 @@

权重分档说明(来自 policy.yaml → yara_rule_weights)

critical → weight=85 - high → weight=65 - medium → weight=45 - low → weight=25 - 未配置的规则默认 weight=50 + high → weight=60 + medium → weight=30 + low → weight=10 + 未列出的规则沿用 rules/*.yar 中 meta.risk_level 推导出的同档权重
diff --git a/config/policy.yaml b/config/policy.yaml index a0caf8f..9397ed6 100644 --- a/config/policy.yaml +++ b/config/policy.yaml @@ -67,8 +67,6 @@ zero_tolerance_rules: - ReverseShellPowershell # PowerShell 反弹 Shell (-enc) - CredentialStealer # 凭据窃取工具 (mimikatz/sekurlsa/wce/...) - CredentialExport # 凭据导出命令 (reg save/reg export/...) - - RemoteDownloadUnix # Unix 远程下载执行 (curl|wget|certutil) - - RemoteDownloadPowerShell # PowerShell 远程下载执行 (IWR/DownloadString) # ---------- 文件类型白名单 ---------- # 只允许这些类型通过。空列表表示不限制(不推荐生产使用)。 @@ -217,6 +215,12 @@ sandbox_required_extensions: # rule_name: [action, reason_override] # action 取值:pass / pass_with_flag / manual_review / reject / reject_and_notify rule_overrides: + # 光闸口径:EXE/DLL/脚本一律不自动放行,最低人工复核。 + # 用 override 而不是靠 weight=30 恰好等于 review_score 下界来维持—— + # 数值巧合意味着把下界调到 >35 或把权重降一点,干净可执行文件就会静默放行。 + DangerousExecutableExtension: + action: manual_review + reason: "可执行/脚本类型文件,按光闸口径最低转人工复核" VBAShellInvocation: action: manual_review reason: "宏调用 Shell 但无自动执行入口,需人工复核" @@ -255,58 +259,60 @@ notification: max_reason_length: 500 # ---------- YARA 规则权重映射 ---------- -# 按规则名精细设定权重(0-100),覆盖 binary.py 里的默认 weight=50 -# 命中但不在下列名单中的规则 weight 保持默认值 50 -# 严重度分档由规则内 meta.risk_level 决定(critical/high/medium/low) +# 档位权重与 rules/*.yar 的 meta.risk_level 一一对应(由 binary.py 读取 meta 得到 +# 默认严重度,再用本表覆盖为固定权重,保证规则文件与策略只有一处真相)。 +# critical=85(单独命中即 reject)/ high=60 / medium=30 / low=10 +# 只有 critical 一档可以独立越过 block_score=70;其余档位需与其他信号叠加。 +# 不在本表的规则:沿用 meta.risk_level 推导出的默认权重。 yara_rule_weights: critical: # weight=85 - - C3_PHP_Webshell_Oneliner - - C3_JSP_Webshell_RuntimeExec - - C3_Webshell_Behinder - C1_PE_ProcessInjection - - C8_Ransomware_Encryption - - C8_Ransomware_LockBit - - C8_Ransomware_BlackCat - - C8_Ransomware_Conti + - C1_PE_RAT_CommandControl + - C3_JSP_Webshell_Deserialization + - C3_JSP_Webshell_RuntimeExec + - C3_PHP_Webshell_Oneliner + - C3_Webshell_Behinder + - C3_Webshell_CommandExecutionEcho + - C3_Webshell_Godzilla - C7_Linux_ReverseShell_Bash - C7_Linux_ReverseShell_NC - C7_Linux_Rootkit_KernelModule - high: # weight=65 + - C8_Ransomware_BlackCat + - C8_Ransomware_Conti + - C8_Ransomware_Encryption + - C8_Ransomware_LockBit + high: # weight=60 + - C1_PE_DLLHijacking + - C1_PE_Downloader_Loader + - C1_PE_LOLBin_Usage + - C3_JSP_Webshell_MemoryShell - C3_PHP_Webshell_Obfuscated - C3_PHP_Webshell_VariableDeformation - - C3_Webshell_CommandExecutionEcho - - C3_Webshell_Godzilla - - C1_PE_Downloader_Loader - - C1_PE_RAT_CommandControl - - C1_PE_LOLBin_Usage - - C1_PE_Mutex_Persistence - - C7_Linux_ReverseShell_Python - - C7_Linux_ReverseShell_Perl - - C7_Linux_ReverseShell_Socat - - C7_Linux_Persistence_Crontab - - C7_Linux_Persistence_SystemD + - C3_Webshell_ASP_Classic + - C3_Webshell_ASPNET + - C3_Webshell_AntSword - C7_Linux_Persistence_AuthorizedKeys + - C7_Linux_Persistence_Crontab - C7_Linux_Persistence_LD_PRELOAD - - C8_Ransomware_VolumeShadow - - C8_Ransomware_Note_Filename + - C7_Linux_Persistence_SystemD + - C7_Linux_ReverseShell_Perl + - C7_Linux_ReverseShell_Python + - C7_Linux_ReverseShell_Socat + - C8_Mining_Monero - C8_Ransomware_Ryuk_Pseudo - medium: # weight=45 - - C9_Packer_UPX - - C9_Packer_Themida_VMP - - C1_PE_DLLHijacking + - C8_Ransomware_VolumeShadow + medium: # weight=30 - C1_PE_EntropyHighNetwork + - C1_PE_Mutex_Persistence - C7_Linux_Miner_Generic + - C7_Linux_ReverseShell_Misc - C7_Misc_Ruby_Lua_ReverseShell - C7_Network_Tools_Malicious - - C3_Webshell_AntSword - - C3_Webshell_ASPNET - - C3_Webshell_ASP_Classic - - C3_JSP_Webshell_Deserialization - - C3_JSP_Webshell_MemoryShell - - C3_Webshell_CommandExecutionEcho + - C8_Ransomware_Note_Filename + - C9_Packer_Themida_VMP + low: # weight=10 - C8_Ransomware_Extension_Blacklist - low: # weight=25 - - C7_Linux_ReverseShell_Misc + - C9_Packer_UPX # ---------- 内网特征词表引擎(P1)---------- @@ -339,26 +345,55 @@ keyword_rules: # 恶意行为特征词表(覆盖 CSV L3 全部 25 条用例) # ================================================================ - # ---- 远程下载执行 ---- + # ---- 远程下载即执行(要有完整动作链,裸词不算;README 里的安装命令进复核而非直接拒)---- + # cross_line:命令换行/续行是本类特征最常见的绕过(curl \ + 下一行 | sh), + # 拼接后仍要求"下载工具 + 执行动作"同框,散文里的多行文本不会凑出这条链。 + - name: RemoteDownloadExec + match: regex + pattern: "(?:\\b(?:curl|wget)\\b[^\\n]{0,120}\\|\\s*(?:ba|z|k|da)?sh\\b|\\b(?:curl|wget)\\b[^\\n]{0,120}(?:&&|;)\\s*(?:chmod\\s+\\+x|\\./)|(?:^|[^\\w$-])(?:iex|invoke-expression)\\b[^\\n]{0,60}(?:\\biwr\\b|invoke\\s*[-_]?\\s*web\\s*[-_]?\\s*request|invoke\\s*[-_]?\\s*rest\\s*[-_]?\\s*method|downloadstring)|\\b(?:iwr|invoke\\s*[-_]?\\s*web\\s*[-_]?\\s*request|invoke\\s*[-_]?\\s*rest\\s*[-_]?\\s*method)\\b[^\\n]{0,120}\\|\\s*(?:iex|invoke-expression)\\b|certutil\\s+[-/]urlcache[^\\n]{0,60}\\.exe\\b|mkfifo[^\\n]{0,60}(?:\\bnc\\s|\\bnetcat\\b))" + ignore_case: true + cross_line: true + weight: 40 + severity: critical + + # ---- 下载并落地成可执行文件(dropper 行为,比"下载即执行"弱一档但仍高危)---- + - name: PayloadDropToDisk + match: regex + pattern: "(?:\\b(?:curl|wget)\\b[^\\n]{0,120}(?:-o|-O|--output)\\s+\\S*\\.(?:exe|dll|ps1|bat|cmd|vbs|jar|msi|so|bin)\\b|\\b(?:i\\s*[-_]?\\s*wr|invoke\\s*[-_]?\\s*web\\s*[-_]?\\s*request|invoke\\s*[-_]?\\s*rest\\s*[-_]?\\s*method)\\b[^\\n]{0,120}-o(?:utfile)?\\s+\\S*\\.(?:exe|dll|ps1|bat|cmd|vbs)\\b|\\b(?:new-object|activexobject)\\s+system\\.net\\.webclient[^\\n]{0,80}downloadfile)" + ignore_case: true + cross_line: true + weight: 45 + severity: critical + + # ---- 外部下载动作(cmdlet/API + 无执行链):计分进复核,不再零容忍 ---- + # cmdlet 名(Invoke-WebRequest/Invoke-RestMethod/BitsAdmin)是 PowerShell 专有写法, + # 单独出现即进复核。.NET/Win32 的方法名(DownloadString/DownloadFile)不一样—— + # 它们存在于所有托管程序的标识符表里,裸匹配实测把 nvidiaInspector.exe 的 + # `add_DownloadFileCompleted`、`DownloadFileAsync` 各算一次命中,白送 30 分, + # 把 45 分的人工复核顶成 75 分拒绝。所以方法名必须与 URL/IP 同框才算下载。 - name: RemoteDownloadPowerShell match: regex - pattern: "(Invoke-WebRequest|Invoke-RestMethod|DownloadString|DownloadFile|Start-BitsTransfer|BitsAdmin)" - ignore_case: true - weight: 35 - severity: high - - - name: RemoteDownloadUnix - match: regex - pattern: "(\\bwget\\b|\\bcurl\\b|\\bcertutil\\s+-urlcache\\s+-split|\\bfetch\\b)" + pattern: "(?:\\b(?:Invoke-WebRequest|Invoke-RestMethod|Start-BitsTransfer|BitsAdmin)\\b|\\bDownload(?:String|File)\\b[^\\n]{0,40}(?:https?://|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(?:https?://|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})[^\\n]{0,40}\\bDownload(?:String|File)\\b)" ignore_case: true weight: 30 - severity: high + severity: medium + + # ---- 下载工具 + 明确地址:只认"工具与 URL/IP 同框",裸 -o 参数不算 ---- + # 旧写法里的 `curl|wget -O/-o 任意值` 会命中任何写成两段的安装文档,去掉。 + - name: RemoteDownloadUnix + match: regex + pattern: "(?:\\b(?:curl|wget|certutil)\\b[^\\n]{0,80}(?:https?://|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}))" + ignore_case: true + weight: 30 + severity: medium # ---- 反弹 Shell ---- - name: ReverseShellNetcat match: regex - # nc/ncat 后必须跟参数 (如 -e/-l/-p/-v/-s),单独 nc 在二进制里必然误报 - pattern: "(\\bnc\\s+-[elpvs]\\w*|\\bncat\\s+-\\w+|\\bnetcat\\b|/dev/tcp/|bash\\s+-i\\s+>&|sh\\s+-i\\s+>&)" + # 只认「执行原语」形态:nc/ncat/netcat 带 -e/-c(把 shell 交给连接), + # 或 /dev/tcp、bash -i >& 这类内核级反连。裸 nc / nc -l / nc -z 是运维常用, + # 出现在速查文档和二进制字符串表里必然误报,不计分。 + pattern: "(\\b(?:nc|ncat|netcat)\\s+(?:[^\\n]{0,60}\\s)?-[ec]\\w*\\s*(?:/bin/|/usr/|cmd\\.exe\\b|\\S*sh\\b)|/dev/tcp/|bash\\s+-i\\s+>&|sh\\s+-i\\s+>&)" ignore_case: true weight: 40 severity: critical @@ -430,6 +465,32 @@ keyword_whitelist: - ".senmesh.com" - "repo.internal" - "mirror.internal" + - rule: RemoteDownloadExec # 下载即执行:内网源站安装脚本按白名单放行 + allowed_keywords: + - "192.168." + - "10." + - "172.16." + - "172.17." + - "172.18." + - "172.19." + - "172.2" + - "172.3" + - ".senmesh.com" + - "repo.internal" + - "mirror.internal" + - rule: PayloadDropToDisk # 下载落地可执行文件:内网源站安装包放行 + 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." diff --git a/docs/策略配置说明.md b/docs/策略配置说明.md index 85abdb7..8fa171b 100644 --- a/docs/策略配置说明.md +++ b/docs/策略配置说明.md @@ -193,7 +193,7 @@ rule_overrides: |---|---| | `pass` | 放行 | | `pass_with_flag` | 放行并标记 | -| `manual_review` | 转人工复核(同时把评分提升至 65,确保落入复核区间) | +| `manual_review` | 处置地板:该文件最终不低于人工复核(评分不变,低于复核区间时只抬处置) | | `reject` | 拒绝 | | `reject_and_notify` | 拒绝并通知(当前实现等同 `reject`) | @@ -201,7 +201,9 @@ rule_overrides: 1. 命中 `rule_overrides` 的规则会**优先于评分**处理 2. 若 `action` 为 `reject` 类 → 立即返回拒绝结论,不再累加评分 -3. 若 `action` 为 `manual_review` → 评分提升至 65 并继续后续裁决 +3. 若 `action` 为 `manual_review` → 登记"复核地板",规则自身权重照常累加,评分累加与阈值裁决完成后,处置若低于 `manual_review` 才抬到 `manual_review`(已判 `reject` 的不会被降回来) + +> 旧实现是"评分提升至 65",有两个副作用:注进去的 65 分本身不是证据,再叠加任意 ≥5 分的噪声信号就会越过 `block_score=70` 变成拒绝;同时该规则自身的权重被跳过不再计分,控制台显示的分数与真实证据脱钩。现改为处置地板后,分数只反映权重,"不放行"由地板保证,与 `review_score` 阈值数值解耦——`DangerousExecutableExtension`(光闸口径:EXE/DLL/脚本不自动放行)正是靠这条地板生效,而不是靠 `weight=30/35` 恰好等于 `review_score` 下界。 **为什么用覆盖而不是纯评分**:像 `PathTraversal`、`DDEAutoExecution` 这类信号,一旦出现就足以判定不可放行,不应依赖其他信号累加来"凑够"阈值。 diff --git a/tests/samples/index.json b/tests/samples/index.json index fac3c82..f1b2fb4 100644 --- a/tests/samples/index.json +++ b/tests/samples/index.json @@ -1047,14 +1047,14 @@ "filename": "L5-005_injection_imports.exe", "case_id": "L5-005", "category": "L5", - "expected_verdict": "reject", + "expected_verdict": "review", "expected_rules": [ "DangerousExecutableExtension", "PEArchInfo", "SuspiciousTimestamp", "UnsignedExecutable" ], - "description": "注入组合导入(需要真实 pefile 引入 API)", + "description": "注入组合导入占位样本(928B 假 PE,实际没有导入表)。旧策略靠 载体45+时间戳20+未签名10+架构10 累加到 85 才判 reject,属于权重堆叠误报;权重收紧后应停在人工复核。真实注入器由 YARA C1_PE_ProcessInjection(critical=85) 直接拦截。", "_scan_verdict": "malicious" }, {