- 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
178 lines
7.3 KiB
Python
178 lines
7.3 KiB
Python
"""默认策略(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"]
|