feat(chv): 对齐 Muse P5 纵深——eBPF 污点传播 + 五档严格能力 + 对话外审批 + 默认策略

- taint.py:污点追踪器(登记/污点不可逆/子进程继承/审计)
- ebpf_taint.py:eBPF 程序源码 + 加载器(能力探测 + 降级);
  本机实测加载被 seccomp 挡(bpf() EPERM),报 blocked 不冒充
- capabilities.py:五档严格能力(一次/会话/任务/时限/永久,
  三元组绑定 + 精确匹配 + 撤销;permanent 默认不签发)
- policy.py:默认策略规则表 + 谨慎度三档(写操作与敏感路径
  永远不自动放行,规则变化审计)
- approval_channel.py:对话外审批通道(nonce + 15 分钟过期 +
  HMAC,答复一次有效,对话伪造无效)
- sentinel.py 接线:evaluate() 可选接 taint/policy/tier/scope,
  不传时行为与 P3 一致;自动放行步骤 + 档位签发回落单次;
  Capability 基类属性放宽为 Optional(None = 不限)
- 新测试 61 项;全量 cloud 套件 1958 项 OK(skipped=8)

真机待验证(未做,排队在 round-11 之后):见
docs/chv-muse-p5-gap.md §4
This commit is contained in:
host committed 2026-11-06 22:25:55 +00:00
1 parent ee456e6625
commit 25bcd355f8
11 files changed
+2436 -48

No files matched your search

+177
View File
@@ -0,0 +1,177 @@
"""默认策略(feat-chv-muse-p5-B,叶子模块,纯标准库)。
默认策略规则表 + 谨慎度三档(``DefaultPolicy``)。只读方法
(GET/HEAD)+ 已允许主机/路径 + 低风险判定(无 body 写操作、
无敏感路径段)→ 自动放行候选;其余走正常审批流。
谨慎度:
- default(默认):已允许主机 + 路径前缀匹配即候选;
- cautious:自动放行收窄——只放行已明确允许过的精确主机 +
精确路径(前缀规则不再放行),ask 增多;
- permissive:放宽自动放行范围——只读 + 非敏感路径即候选,
不必先前已允许;但写操作(POST/PUT/DELETE/PATCH)与敏感
路径(/admin、/settings、/login、token 相关)永远不自动放行。
``auto_allow_eligible(request, tainted)`` 只是“候选”判定,不
是放行 verdict:污点进程一律 False(与 P5-A 污点追踪器口径
一致,tainted 由调用方传);未知连接器仍由 SentinelPolicy
的 evaluate() 挡。档位切换与规则增删(``set_caution`` /
``add_rule`` / ``remove_rule``)每次变化记审计事件
``sentinel_policy_changed``(旧值 → 新值,无值)。
"""
from collections.abc import Iterable
from cloud.chv.sentinel import EgressRequest
READ_METHODS = frozenset({"GET", "HEAD"})
WRITE_METHODS = frozenset({"POST", "PUT", "DELETE", "PATCH"})
CAUTION_LEVELS = ("default", "cautious", "permissive")
_SENSITIVE_MARKERS = ("/admin", "/settings", "/login", "token")
_EVENT_POLICY_CHANGED = "sentinel_policy_changed"
def _is_sensitive_path(path: str) -> bool:
"""敏感路径:/admin、/settings、/login 或 token 相关。"""
lowered = (path or "").lower()
return any(m in lowered for m in _SENSITIVE_MARKERS)
def _rule_key(connector: str, host: str, path: str,
methods: frozenset) -> tuple:
"""规则内部形态:(连接器,主机小写,路径,方法集)。"""
return (connector, (host or "").lower(), path, methods)
def _rule_dict(rule: tuple) -> dict:
"""规则的审计/展示形态(只含连接器/主机/路径/方法)。"""
return {"connector": rule[0], "host": rule[1],
"path": rule[2], "methods": sorted(rule[3])}
def _rule_matches(rule: tuple, connector: str, host: str,
path: str,
methods: Iterable[str] | None) -> bool:
"""该规则是否命中(连接器,主机,路径)[+ 方法]。"""
if rule[0] != connector or rule[1] != (host or "").lower():
return False
if rule[2] != path:
return False
if methods is None:
return True
return rule[3] == frozenset(m.upper() for m in methods)
class DefaultPolicy:
"""默认策略:规则表(已允许主机 + 路径)+ 谨慎度三档。
规则表由调用方播种(``allow_entries``,每条是
(连接器,主机,路径前缀)三元组,方法默认只读)或
``add_rule`` 增补;``audit_log`` 收全部策略变化审计。
"""
def __init__(self, caution: str = "default",
allow_entries: Iterable[tuple[str, str, str]] = (),
audit: list | None = None) -> None:
if caution not in CAUTION_LEVELS:
raise ValueError("unknown_caution:%s" % caution)
self.caution = caution
self._rules: list = []
for connector, host, path in allow_entries:
self._rules.append(_rule_key(
connector, host, path, READ_METHODS))
self.audit_log = audit if audit is not None else []
def _audit_change(self, change: str, old_value: object,
new_value: object) -> None:
"""记一条策略变化审计(旧值 → 新值,无值)。"""
self.audit_log.append(
{"event": _EVENT_POLICY_CHANGED, "change": change,
"old_value": old_value, "new_value": new_value})
def set_caution(self, level: str) -> bool:
"""切换谨慎度:变化记审计并返回 True;非法档位抛
ValueError;与现值相同则不记审计、返回 False。"""
if level not in CAUTION_LEVELS:
raise ValueError("unknown_caution:%s" % level)
if level == self.caution:
return False
old = self.caution
self.caution = level
self._audit_change("set_caution", old, level)
return True
def add_rule(self, connector: str, host: str, path: str,
methods: Iterable[str] = READ_METHODS) -> bool:
"""增一条已允许规则(连接器 + 主机 + 路径前缀 + 方法)。
已存在相同规则返回 False(不记审计);新增记审计。"""
rule = _rule_key(connector, host, path,
frozenset(m.upper() for m in methods))
if rule in self._rules:
return False
self._rules.append(rule)
self._audit_change("add_rule", None, _rule_dict(rule))
return True
def remove_rule(self, connector: str, host: str, path: str,
methods: Iterable[str] | None = None) -> bool:
"""删规则:methods 为 None 时按(连接器,主机,路径)
删全部方法变体;否则只删精确匹配。删到记审计。"""
removed = False
kept = []
for rule in self._rules:
if _rule_matches(rule, connector, host, path, methods):
removed = True
self._audit_change("remove_rule",
_rule_dict(rule), None)
else:
kept.append(rule)
self._rules = kept
return removed
def list_rules(self) -> list:
"""全部已允许规则(副本)。"""
return [_rule_dict(rule) for rule in self._rules]
def _allowlisted(self, request: EgressRequest,
exact: bool) -> bool:
"""主机 + 路径是否在规则表内(exact 为真时路径须全等,
否则前缀匹配即算;方法也须在规则方法集内)。"""
for rule in self._rules:
if rule[0] != request.connector:
continue
if rule[1] != request.host:
continue
if request.method not in rule[3]:
continue
if exact:
if request.path == rule[2]:
return True
elif request.path.startswith(rule[2]):
return True
return False
def auto_allow_eligible(self, request: EgressRequest,
tainted: bool) -> bool:
"""是否自动放行候选(只是候选判定,不是 verdict)。
污点进程一律 False;写操作与敏感路径在任何档位都不
自动放行;default 档须已允许主机 + 路径前缀;cautious
档须精确主机 + 精确路径;permissive 档只读 + 非敏感
即候选(不必先前已允许)。
"""
if tainted:
return False
if request.method not in READ_METHODS:
return False
if _is_sensitive_path(request.path):
return False
if self.caution == "permissive":
return True
if self.caution == "cautious":
return self._allowlisted(request, exact=True)
return self._allowlisted(request, exact=False)
__all__ = ["READ_METHODS", "WRITE_METHODS", "CAUTION_LEVELS",
"DefaultPolicy"]