Files
codex-app-server/cloud/chv/policy.py
T
host 25bcd355f8 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
2026-11-06 22:25:55 +00:00

178 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""默认策略(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"]