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,171 @@
|
||||
"""污点追踪器(对齐 Muse P5-A,叶子模块,纯标准库)。
|
||||
|
||||
Muse 口径(见 docs/chv-muse-p5-gap.md §1 污点出口行):每个工具
|
||||
执行进程初始干净,读取用户数据后变“污点”;干净且符合窄范围
|
||||
自动放行策略并通过出站 URL 其他检查的请求可免打扰放行,污点
|
||||
或不可验证进程失去自动放行、回到正常审批流。
|
||||
|
||||
本模块只是用户态追踪器,不是强制层:
|
||||
- “读用户数据”事件靠调用方上报(工具执行层在读用户数据前
|
||||
调 `mark_tainted`,或 eBPF 通了之后由 LSM hook 回调上报)。
|
||||
没接 LSM hook 之前漏报是可能的,本模块不宣称“所有读用户
|
||||
数据的行为都被捕获”。
|
||||
- 污点是单向的:没有清除入口,只能随进程退出消失。
|
||||
- `source` 只记“哪类用户数据”(见 `_SOURCE_CATEGORIES`),
|
||||
绝不记内容;不在分类表里的上报值一律归一化为 `unknown`,
|
||||
原始字符串不会落进任何记录。审计记录里只有事件名、pid、
|
||||
原因与分类,绝无用户数据内容、无凭证值。
|
||||
- 未知 pid(未注册)一律不可自动放行:`is_tainted` 对它报
|
||||
False(没证据说它污),`auto_allow_eligible` 对它也报
|
||||
False(没证据说它干净可信)。
|
||||
"""
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
# 允许的 source 分类:只记“哪类用户数据”,不记内容。
|
||||
_SOURCE_CATEGORIES = frozenset({
|
||||
"gitea_repo",
|
||||
"user_file",
|
||||
"user_memory",
|
||||
"chat_history",
|
||||
"credential_vault",
|
||||
"tool_output",
|
||||
"unknown",
|
||||
})
|
||||
|
||||
# 审计事件名:沿用 P3 的 `sentinel_*` 契约(见 gap §1 审计行)。
|
||||
_EVENT_MARKED = "sentinel_taint_marked"
|
||||
_EVENT_INHERITED = "sentinel_taint_inherited"
|
||||
_EVENT_REGISTERED = "sentinel_taint_registered"
|
||||
_EVENT_RELEASED = "sentinel_taint_released"
|
||||
|
||||
|
||||
def _normalize_source(source: str) -> str:
|
||||
"""source 归一化为分类表里的值,不在表里的记 `unknown`。
|
||||
|
||||
调用方误把内容当 source 传进来时,原始字符串在这里就被
|
||||
丢掉,绝不会进入进程记录或审计记录。"""
|
||||
value = (source or "").strip().lower()
|
||||
if value in _SOURCE_CATEGORIES:
|
||||
return value
|
||||
return "unknown"
|
||||
|
||||
|
||||
class TaintTracker:
|
||||
"""进程污点两态追踪器(内存态,进程退出即清)。
|
||||
|
||||
每个进程一条记录:pid、cgroup、连接器、用例、是否污点。
|
||||
污点只能由 `mark_tainted` 置位、由 `fork_child` 继承、由
|
||||
`process_exit` 随进程消失;没有反向清除入口。
|
||||
"""
|
||||
|
||||
def __init__(self, clock: Callable[[], float] | None = None
|
||||
) -> None:
|
||||
self._records: dict[int, dict] = {}
|
||||
self._audit: list[dict] = []
|
||||
self._clock = clock or time.time
|
||||
|
||||
def _emit(self, event: str, pid: int, reason: str,
|
||||
source: str = "") -> None:
|
||||
"""记一条审计(事件名 + pid + 原因 + 分类,无内容)"""
|
||||
record = {"event": event, "pid": pid, "reason": reason,
|
||||
"ts": self._clock()}
|
||||
if source:
|
||||
record["source"] = source
|
||||
self._audit.append(record)
|
||||
|
||||
def register(self, pid: int, cgroup: str = "",
|
||||
connector: str = "", use_case: str = "") -> bool:
|
||||
"""登记一个干净进程。pid 非法或已登记返回 False。"""
|
||||
if pid <= 0 or pid in self._records:
|
||||
return False
|
||||
self._records[pid] = {
|
||||
"pid": pid, "cgroup": cgroup,
|
||||
"connector": connector, "use_case": use_case,
|
||||
"tainted": False,
|
||||
}
|
||||
self._emit(_EVENT_REGISTERED, pid, "process_registered")
|
||||
return True
|
||||
|
||||
def mark_tainted(self, pid: int, source: str) -> bool:
|
||||
"""上报“读用户数据”事件:该进程变污点,不可逆。
|
||||
|
||||
未登记的 pid 也接受上报(fail-closed:读了用户数据的
|
||||
进程先记为污点,不等它补登记);source 只存分类。"""
|
||||
category = _normalize_source(source)
|
||||
record = self._records.get(pid)
|
||||
if record is None:
|
||||
self._records[pid] = {
|
||||
"pid": pid, "cgroup": "", "connector": "",
|
||||
"use_case": "", "tainted": True,
|
||||
}
|
||||
else:
|
||||
record["tainted"] = True
|
||||
reason = "read_user_data:" + category
|
||||
self._emit(_EVENT_MARKED, pid, reason, source=category)
|
||||
return True
|
||||
|
||||
def fork_child(self, parent_pid: int, child_pid: int) -> bool:
|
||||
"""子进程继承污点:父污则子污,父干净则子干净。
|
||||
|
||||
父未登记或子 pid 非法返回 False,不建记录;子已登记
|
||||
时只改它的污点位,不覆盖它自己的 cgroup/连接器/用例。"""
|
||||
if child_pid <= 0 or child_pid == parent_pid:
|
||||
return False
|
||||
parent = self._records.get(parent_pid)
|
||||
if parent is None:
|
||||
return False
|
||||
child = self._records.get(child_pid)
|
||||
if child is None:
|
||||
self._records[child_pid] = {
|
||||
"pid": child_pid, "cgroup": parent["cgroup"],
|
||||
"connector": parent["connector"],
|
||||
"use_case": parent["use_case"],
|
||||
"tainted": parent["tainted"],
|
||||
}
|
||||
else:
|
||||
child["tainted"] = parent["tainted"]
|
||||
if parent["tainted"]:
|
||||
self._emit(_EVENT_INHERITED, child_pid,
|
||||
"inherited_from_parent:%d" % parent_pid)
|
||||
return True
|
||||
|
||||
def process_exit(self, pid: int) -> bool:
|
||||
"""进程退出:记录连同污点一起消失。未知 pid 返回 False。"""
|
||||
if pid not in self._records:
|
||||
return False
|
||||
del self._records[pid]
|
||||
self._emit(_EVENT_RELEASED, pid, "process_exit")
|
||||
return True
|
||||
|
||||
def is_tainted(self, pid: int) -> bool:
|
||||
"""该进程是否污点。未知 pid 报 False(无证据说它污)。"""
|
||||
record = self._records.get(pid)
|
||||
return bool(record and record["tainted"])
|
||||
|
||||
def is_registered(self, pid: int) -> bool:
|
||||
"""该 pid 是否已登记。"""
|
||||
return pid in self._records
|
||||
|
||||
def state(self, pid: int) -> dict | None:
|
||||
"""该进程记录的副本(无记录返回 None)。"""
|
||||
record = self._records.get(pid)
|
||||
if record is None:
|
||||
return None
|
||||
return dict(record)
|
||||
|
||||
def auto_allow_eligible(self, pid: int) -> bool:
|
||||
"""出口自动放行资格:已登记且干净才 True。
|
||||
|
||||
污点进程一律 False;未知 pid(不可验证)也一律 False。
|
||||
这只是决策输入,不放行任何请求——放行仍须走 Sentinel
|
||||
的出站 URL 其他检查(见 gap §1)。"""
|
||||
record = self._records.get(pid)
|
||||
if record is None:
|
||||
return False
|
||||
return not record["tainted"]
|
||||
|
||||
def audit(self) -> list:
|
||||
"""全部审计记录的副本(事件名 + pid + 原因 + 分类)。"""
|
||||
return [dict(record) for record in self._audit]
|
||||
Reference in new issue
Block a user