"""默认策略(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"]