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:
1 parent
ee456e6625
commit
25bcd355f8
11 files changed
+2436
-48
No files matched your search
@@ -0,0 +1,276 @@
|
||||
"""五档严格能力(feat-chv-muse-p5-B,叶子模块,纯标准库)。
|
||||
|
||||
建在 P3 的 ``Capability`` 之上(继承,不重写绑定语义):P3 只给
|
||||
单次 + 300 秒一档;本模块加五档与用例维度,签发/使用/过期/
|
||||
超次/撤销全审计(事件名 + 能力 id + 原因,绝无凭证值与用户
|
||||
数据内容)。
|
||||
|
||||
档位(``TIERS``)与签发默认值(``CapabilityIssuer.issue``):
|
||||
- one-shot:单次、300 秒(P3 默认);
|
||||
- session:会话内多次、无固定次数,会话 id 绑定,默认 3600 秒;
|
||||
- task:任务内多次、无固定次数,任务 id 绑定,默认 3600 秒;
|
||||
- timed:时限内多次、无固定次数,须显式 ttl;
|
||||
- permanent:永久、无过期无次数上限——默认不签发,须显式
|
||||
``allow_permanent=True`` + 审计,否则 ``ValueError``。
|
||||
|
||||
能力绑定 = {连接器,主机,用例} 三元组 + 方法/路径规则 +
|
||||
次数/过期。``matches(request, now, scope)`` 精确匹配(``scope``
|
||||
须按关键字传,``now`` 仍占第二位置参数,与 P3 旧口径一致):
|
||||
用例、方法、路径、会话/任务绑定、有效期、剩余次数任一项越界
|
||||
即 False(并记审计)。``scope`` 是 dict 或带属性的对象,含
|
||||
``use_case`` / ``session_id`` / ``task_id``;没给 scope(None)
|
||||
按空 scope 计,即用例/会话/任务绑定一律不匹配(fail-closed)。
|
||||
"""
|
||||
|
||||
import time
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
from cloud.chv.sentinel import (
|
||||
DEFAULT_CAPABILITY_TTL_SECONDS,
|
||||
Capability,
|
||||
EgressRequest,
|
||||
_path_allowed,
|
||||
)
|
||||
|
||||
TIERS = ("one-shot", "session", "task", "timed", "permanent")
|
||||
DEFAULT_SESSION_TTL_SECONDS = 3600
|
||||
DEFAULT_TASK_TTL_SECONDS = 3600
|
||||
|
||||
_EVENT_ISSUED = "sentinel_capability_issued"
|
||||
_EVENT_USED = "sentinel_capability_used"
|
||||
_EVENT_DENIED = "sentinel_capability_denied"
|
||||
_EVENT_EXPIRED = "sentinel_capability_expired"
|
||||
_EVENT_REVOKED = "sentinel_capability_revoked"
|
||||
|
||||
|
||||
def _scope_get(scope: object | None, key: str) -> object | None:
|
||||
"""从 scope(dict 或带属性的对象)取一键,缺失即 None。"""
|
||||
if scope is None:
|
||||
return None
|
||||
if isinstance(scope, dict):
|
||||
return scope.get(key)
|
||||
return getattr(scope, key, None)
|
||||
|
||||
|
||||
def _audit_record(cap: "ScopedCapability", event: str,
|
||||
reason: str) -> dict:
|
||||
"""审计记录:事件名 + 能力 id + 原因,无值。"""
|
||||
return {"event": event, "capability_id": cap.id,
|
||||
"tier": cap.tier, "reason": reason,
|
||||
"connector": cap.connector, "host": cap.host,
|
||||
"use_case": cap.use_case}
|
||||
|
||||
|
||||
class ScopedCapability(Capability):
|
||||
"""五档严格能力:P3 绑定 + 用例三元组 + 会话/任务绑定。
|
||||
|
||||
``max_uses=None`` 表示无固定次数(session/task/timed/
|
||||
permanent 的默认);``expires_at=None`` 表示永不过期
|
||||
(仅 permanent)。``revoked`` 后 ``matches`` 恒 False。
|
||||
``audit_log`` 与签发方共享:签发/使用/过期/超次/撤销
|
||||
全记进同一份审计(事件名 + 能力 id + 原因,无值)。
|
||||
"""
|
||||
|
||||
def __init__(self, connector: str, host: str,
|
||||
methods: Iterable[str],
|
||||
path_rules: Iterable[tuple[str, str, str]],
|
||||
tier: str, use_case: str,
|
||||
session_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
ttl_seconds: int | None = None,
|
||||
max_uses: int | None = None,
|
||||
now: float | None = None,
|
||||
audit: list | None = None) -> None:
|
||||
super().__init__(
|
||||
connector, host, methods, path_rules,
|
||||
ttl_seconds=(ttl_seconds if ttl_seconds is not None
|
||||
else 0),
|
||||
max_uses=(max_uses if max_uses is not None else 1),
|
||||
now=now)
|
||||
self.tier = tier
|
||||
self.use_case = use_case
|
||||
self.session_id = session_id
|
||||
self.task_id = task_id
|
||||
self.revoked = False
|
||||
self.max_uses = max_uses
|
||||
self.uses_left = max_uses
|
||||
if ttl_seconds is None:
|
||||
self.expires_at = None
|
||||
self.audit_log = audit if audit is not None else []
|
||||
|
||||
def _mismatch_reason(self, request: EgressRequest,
|
||||
scope: object | None,
|
||||
now: float | None) -> str | None:
|
||||
"""越界原因(None 表示在绑定内)。检查顺序固定:
|
||||
撤销 → 用例 → 会话/任务 → 连接器/主机/方法/路径 →
|
||||
过期 → 次数。"""
|
||||
if self.revoked:
|
||||
return "revoked"
|
||||
if _scope_get(scope, "use_case") != self.use_case:
|
||||
return "use_case_mismatch"
|
||||
if self.tier == "session" and (
|
||||
_scope_get(scope, "session_id") != self.session_id):
|
||||
return "session_mismatch"
|
||||
if self.tier == "task" and (
|
||||
_scope_get(scope, "task_id") != self.task_id):
|
||||
return "task_mismatch"
|
||||
if request.connector != self.connector:
|
||||
return "connector_mismatch"
|
||||
if request.host != self.host:
|
||||
return "host_mismatch"
|
||||
if request.method not in self.methods:
|
||||
return "method_not_allowed"
|
||||
if not _path_allowed(request, self.path_rules):
|
||||
return "path_not_allowed"
|
||||
at = now if now is not None else time.time()
|
||||
if self.expires_at is not None and at >= self.expires_at:
|
||||
return "expired"
|
||||
if self.uses_left is not None and self.uses_left <= 0:
|
||||
return "uses_exhausted"
|
||||
return None
|
||||
|
||||
def _audit(self, event: str, reason: str) -> None:
|
||||
"""记一条审计(事件名 + 能力 id + 原因,无值)。"""
|
||||
self.audit_log.append(_audit_record(self, event, reason))
|
||||
|
||||
def matches(self, request: EgressRequest,
|
||||
now: float | None = None,
|
||||
scope: object | None = None) -> bool:
|
||||
"""该请求是否在能力绑定内(不消耗次数,只判定)。
|
||||
|
||||
``scope`` 须按关键字传(``now`` 仍占第二位置参数,
|
||||
与 P3 旧口径一致)。越界即 False 并记审计:过期记
|
||||
expired,其余记 denied。
|
||||
"""
|
||||
reason = self._mismatch_reason(request, scope, now)
|
||||
if reason is None:
|
||||
return True
|
||||
event = (_EVENT_EXPIRED if reason == "expired"
|
||||
else _EVENT_DENIED)
|
||||
self._audit(event, reason)
|
||||
return False
|
||||
|
||||
def use(self, request: EgressRequest,
|
||||
now: float | None = None,
|
||||
scope: object | None = None) -> bool:
|
||||
"""消耗一次能力:绑定内才扣次数并返回 True,否则 False
|
||||
(失败原因已由 ``matches`` 记审计,不再重复记)。"""
|
||||
if not self.matches(request, now=now, scope=scope):
|
||||
return False
|
||||
if self.uses_left is not None:
|
||||
self.uses_left -= 1
|
||||
self._audit(_EVENT_USED, "used")
|
||||
return True
|
||||
|
||||
def revoke(self) -> bool:
|
||||
"""撤销:首次返回 True 并记审计;已撤销再调返回 False。
|
||||
撤销后 ``matches`` 恒 False(原因 revoked)。"""
|
||||
if self.revoked:
|
||||
self._audit(_EVENT_DENIED, "already_revoked")
|
||||
return False
|
||||
self.revoked = True
|
||||
self._audit(_EVENT_REVOKED, "revoked")
|
||||
return True
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""审计/展示用绑定描述(只含绑定,不含值)。"""
|
||||
return {"capability_id": self.id, "tier": self.tier,
|
||||
"connector": self.connector, "host": self.host,
|
||||
"use_case": self.use_case,
|
||||
"session_id": self.session_id,
|
||||
"task_id": self.task_id,
|
||||
"methods": sorted(self.methods),
|
||||
"max_uses": self.max_uses,
|
||||
"uses_left": self.uses_left,
|
||||
"expires_at": self.expires_at,
|
||||
"revoked": self.revoked}
|
||||
|
||||
|
||||
class CapabilityIssuer:
|
||||
"""五档能力签发入口:按档位设默认值,审计与签出的能力共享。
|
||||
|
||||
permanent 默认拒绝:须调用时显式 ``allow_permanent=True``,
|
||||
拒绝本身也记审计(denied,原因
|
||||
``permanent_requires_explicit_opt_in``)。
|
||||
"""
|
||||
|
||||
def __init__(self, audit: list | None = None,
|
||||
clock: Callable[[], float] | None = None) -> None:
|
||||
self.audit_log = audit if audit is not None else []
|
||||
self._clock = clock or time.time
|
||||
|
||||
def issue(self, tier: str, connector: str, host: str,
|
||||
methods: Iterable[str],
|
||||
path_rules: Iterable[tuple[str, str, str]],
|
||||
use_case: str, session_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
ttl_seconds: int | None = None,
|
||||
max_uses: int | None = None,
|
||||
now: float | None = None,
|
||||
allow_permanent: bool = False) -> ScopedCapability:
|
||||
"""签发一档能力(按档位设默认值,见模块 docstring)。
|
||||
|
||||
未知档位、空用例、session 缺会话 id、task 缺任务 id、
|
||||
timed 缺 ttl、permanent 未显式开启一律 ValueError;
|
||||
校验失败先记审计(无能力 id)再抛错。
|
||||
"""
|
||||
at = now if now is not None else self._clock()
|
||||
if tier not in TIERS:
|
||||
raise ValueError("unknown_tier:%s" % tier)
|
||||
if not use_case:
|
||||
raise ValueError("use_case_required")
|
||||
try:
|
||||
ttl, uses = self._defaults(
|
||||
tier, session_id, task_id, ttl_seconds, max_uses,
|
||||
allow_permanent)
|
||||
except ValueError as exc:
|
||||
self.audit_log.append(
|
||||
{"event": _EVENT_DENIED, "capability_id": None,
|
||||
"tier": tier, "reason": str(exc),
|
||||
"connector": connector, "host": host,
|
||||
"use_case": use_case})
|
||||
raise
|
||||
cap = ScopedCapability(
|
||||
connector, host, methods, path_rules, tier, use_case,
|
||||
session_id=session_id, task_id=task_id,
|
||||
ttl_seconds=ttl, max_uses=uses, now=at,
|
||||
audit=self.audit_log)
|
||||
cap._audit(_EVENT_ISSUED, "issued")
|
||||
return cap
|
||||
|
||||
def _defaults(self, tier: str, session_id: str | None,
|
||||
task_id: str | None, ttl_seconds: int | None,
|
||||
max_uses: int | None,
|
||||
allow_permanent: bool
|
||||
) -> tuple[int | None, int | None]:
|
||||
"""各档默认值(非法组合抛 ValueError,原因即消息)。
|
||||
|
||||
one-shot:1 次/300 秒;session/task:无固定次数,默认
|
||||
3600 秒;timed:须显式 ttl;permanent:无过期无次数,
|
||||
忽略传入的 ttl/max_uses。"""
|
||||
if tier == "one-shot":
|
||||
return (ttl_seconds if ttl_seconds is not None
|
||||
else DEFAULT_CAPABILITY_TTL_SECONDS,
|
||||
max_uses if max_uses is not None else 1)
|
||||
if tier == "session":
|
||||
if not session_id:
|
||||
raise ValueError("session_id_required")
|
||||
return (ttl_seconds if ttl_seconds is not None
|
||||
else DEFAULT_SESSION_TTL_SECONDS, max_uses)
|
||||
if tier == "task":
|
||||
if not task_id:
|
||||
raise ValueError("task_id_required")
|
||||
return (ttl_seconds if ttl_seconds is not None
|
||||
else DEFAULT_TASK_TTL_SECONDS, max_uses)
|
||||
if tier == "timed":
|
||||
if ttl_seconds is None:
|
||||
raise ValueError("ttl_required")
|
||||
return (ttl_seconds, max_uses)
|
||||
if not allow_permanent:
|
||||
raise ValueError("permanent_requires_explicit_opt_in")
|
||||
return (None, None)
|
||||
|
||||
|
||||
__all__ = ["TIERS", "DEFAULT_SESSION_TTL_SECONDS",
|
||||
"DEFAULT_TASK_TTL_SECONDS", "ScopedCapability",
|
||||
"CapabilityIssuer"]
|
||||
Reference in new issue
Block a user