mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:41:59 +00:00
可验证报告:口径卡取的列是 SQL 里改了名字的遮罩列时,整张卡同样只给哈希
口径卡上记着每个输入的值和代入式,有输入取自遮罩列的整张卡只给哈希;但以前只按列名认:查询写 phone AS tel、经子查询 或 WITH 转一道、套个函数,卡上取 tel 那一格就是号码,卡照样原样嵌进要发给别人的文件。现在那几列算不算遮罩列另按 SQL 认改了名字的(_Store.hidden → _derived_columns,用 inquiry._derived_hidden,和质询、双盲复算同一个认法;SQL 按每种 读得通的读法各认一遍合起来,都读不通的写着遮罩列就整份的列都算,出错时整份的列都算)。同一条查询里没取自遮罩列的 列照常嵌入。新测试覆盖别名、子查询、WITH 加函数三种改名,以及没改名的列、认的过程出错;每处都做过去掉实现测试变红的 验证。
This commit is contained in:
@@ -83,6 +83,7 @@ class _Store:
|
||||
self._raw: dict[str, bytes | None] = {}
|
||||
self._json: dict[str, Any] = {}
|
||||
self._masked: dict[str, bool] = {}
|
||||
self._hidden: dict[str, set[str]] = {}
|
||||
|
||||
def raw(self, artifact: str) -> bytes | None:
|
||||
if artifact not in self._raw:
|
||||
@@ -118,14 +119,24 @@ class _Store:
|
||||
intact=self.intact(artifact))
|
||||
return self._masked[artifact]
|
||||
|
||||
def hidden(self, artifact: str) -> set[str]:
|
||||
"""一份读得出来的查询快照里值取自遮罩列的列(小写):查询当时记下的、数据源现在设的遮罩列,加上 SQL 里给它们
|
||||
改了名字的(_derived_columns)。"""
|
||||
if artifact not in self._hidden:
|
||||
content = self.content(artifact)
|
||||
base = _snapshot_hidden(content, self._source_masks) if isinstance(content, dict) else set()
|
||||
self._hidden[artifact] = _derived_columns(content, base) if base else set()
|
||||
return self._hidden[artifact]
|
||||
|
||||
def card_masked(self, artifact: str, events: Sequence[tuple[int, str, str | None, Any]]) -> bool:
|
||||
"""这张口径卡算不算带遮罩:卡上有一个输入取自遮罩列,或者遮没遮核对不了。卡上记着每个输入的值(cell() 取的格、
|
||||
Agent 字段、按位置取的 vars.res.rows[0][2])和代入式,整张卡不随文件附带——没法只拿掉几个值,拿掉了哈希就对不上。
|
||||
|
||||
认哪个输入取自哪份查询结果的哪几列,和证据面板、推演同一个认法(recompute.input_origin);按位置取的一格顺着
|
||||
取值链从写变量的节点在口径卡算这一次之前的产出起走。核对不了的宁可多挡:快照读不出来、取值链上的产出读不出来、
|
||||
那份结果没存快照只带着数据源名、定位里没写列。认得出、可结果既没带快照的 id 也没带数据源名的(数据整形改过形状)
|
||||
认不出出处,不算。
|
||||
取值链从写变量的节点在口径卡算这一次之前的产出起走。那几列算不算遮罩列,除了列名对上的,SQL 里给遮罩列起了别名、
|
||||
套了函数、经子查询或 WITH 转了一道的也算(hidden,同质询、双盲复算的认法)。核对不了的宁可多挡:快照读不出来、
|
||||
取值链上的产出读不出来、那份结果没存快照只带着数据源名、定位里没写列。认得出、可结果既没带快照的 id 也没带数据源名
|
||||
的(数据整形改过形状)认不出出处,不算。
|
||||
|
||||
卡本身按解析出来的内容认,不看哈希(同 content):服务端的文件被改过,嵌进页面的照样是这份原字节,卡上记着的号码
|
||||
也照样在里面。读得出字节、解析不出一张卡的(截断的 JSON、不是对象)认不出有哪些输入,整张按带遮罩算,和读不出来的
|
||||
@@ -160,7 +171,7 @@ class _Store:
|
||||
return True
|
||||
if not origin.columns:
|
||||
return self.masked(origin.artifact)
|
||||
hidden = _snapshot_hidden(content, self._source_masks)
|
||||
hidden = self.hidden(origin.artifact)
|
||||
return any(c.lower() in hidden for c in origin.columns)
|
||||
|
||||
|
||||
@@ -237,6 +248,27 @@ def _untouched(snapshot: dict[str, Any], hidden: set[str]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _derived_columns(snapshot: dict[str, Any], base: set[str]) -> set[str]:
|
||||
"""一份快照里值取自遮罩列的结果列(小写):遮罩列本身(base),加上 SQL 里给它们起别名、套函数、经子查询或 WITH
|
||||
转一道得来的(inquiry._derived_hidden,质询、双盲复算同一个认法)。SQL 按每种读得通的读法各认一遍、合起来;都读不通
|
||||
的照那一套的办法,写着遮罩列就整份的列都算。出错时整份的列都算(宁可多挡)。"""
|
||||
try:
|
||||
from app.data.sql_semantics import _parse, _read
|
||||
from app.engine.inquiry import _derived_hidden
|
||||
|
||||
sql = snapshot.get("sql")
|
||||
kinds = [kind for kind in _READINGS
|
||||
if isinstance(sql, str) and _parse(sql, _read(kind))[0] is not None] or [None]
|
||||
out = set(base)
|
||||
for kind in kinds:
|
||||
out |= {c.lower() for c in _derived_hidden(dict(snapshot), set(base), kind)[0]}
|
||||
return out
|
||||
except Exception: # noqa: BLE001 - 认遮罩列的那一套出了意外:整份的列都按遮罩算
|
||||
logger.warning("可验证报告:按 SQL 认改了名字的遮罩列时出错,整份快照的列都按遮罩处理", exc_info=True)
|
||||
columns = snapshot.get("columns")
|
||||
return set(base) | ({str(c).lower() for c in columns} if isinstance(columns, list) else set())
|
||||
|
||||
|
||||
def _writes_any(sql: str, names: Iterable[str]) -> bool:
|
||||
"""SQL 原文里有没有写到这些名字:不论大小写,前后不是字母、数字、下划线(引号、括号、点都算隔开)。"""
|
||||
return any(re.search(rf"(?<![0-9A-Za-z_]){re.escape(n)}(?![0-9A-Za-z_])", sql, re.I) for n in names if n)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
以前只有查询快照会因遮罩不随文件附带,口径卡一律原样嵌入:证据面板上已经遮住的号码(cell() 取的遮罩格、按位置取的
|
||||
vars.res.rows[0][1]),在这份要发给别人的文件里照样看得到。认哪个输入取自哪份查询结果的哪几列,和证据面板、推演同一个
|
||||
认法(recompute.input_origin);核对不了的宁可多挡。
|
||||
认法(recompute.input_origin);核对不了的宁可多挡。那几列算不算遮罩列,除了列名对上的,SQL 里给遮罩列起了别名、套了函数、
|
||||
经子查询或 WITH 转了一道的也算(同质询、双盲复算的 inquiry._derived_hidden):卡上取的 tel 可能就是 phone AS tel。
|
||||
|
||||
服务端的卡文件被改过(哈希对不上)也照卡上记的输入认:嵌进去的是原字节,号码照样在里面。读得出字节、却解析不出一张卡
|
||||
(截断的 JSON、不是对象)的,认不出输入,整张按带遮罩算、不嵌:和读不出来的快照按带遮罩算是同一个道理。
|
||||
@@ -89,6 +90,46 @@ def test_a_card_reading_only_unmasked_columns_is_embedded():
|
||||
assert "masked" not in entry and base64.b64decode(entry["b64"]) == stored(card)[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql", [
|
||||
"SELECT name, phone AS tel, spend FROM members",
|
||||
"SELECT name, tel, spend FROM (SELECT name, phone AS tel, spend FROM members) t",
|
||||
"WITH c AS (SELECT name, SUBSTR(phone, 1) AS tel, spend FROM members) SELECT name, tel, spend FROM c",
|
||||
])
|
||||
def test_a_card_reading_a_renamed_masked_column_ships_only_its_id(sql):
|
||||
"""查询给号码起了别名、经子查询或 WITH 转了一道:卡上取的 tel 那一格就是号码。改了名字的遮罩列按 SQL 认(同质询、
|
||||
双盲复算的 inquiry._derived_hidden),整张卡只给 id。以前只按列名认,号码原样进了文件。"""
|
||||
snap = {**SNAP_MASKED, "columns": ["name", "tel", "spend"], "sql": sql}
|
||||
snap_id, raw = stored(snap)
|
||||
item = {**cell_input("tel", PHONE), "artifact": snap_id, "path": "cell(nodes.members, 0, 'tel')"}
|
||||
bundle, entry = bundle_of(card_with([item], expression="len(cell(nodes.members, 0, 'tel'))"),
|
||||
files_changed={snap_id: raw})
|
||||
assert entry.get("masked") is True and "b64" not in entry
|
||||
assert PHONE not in render_html(bundle)
|
||||
|
||||
|
||||
def test_a_card_reading_an_unrenamed_column_of_that_query_is_embedded():
|
||||
"""同一条起了别名的查询,卡上取的是消费额(不取自号码):照常嵌入。"""
|
||||
snap = {**SNAP_MASKED, "columns": ["name", "tel", "spend"], "sql": "SELECT name, phone AS tel, spend FROM members"}
|
||||
snap_id, raw = stored(snap)
|
||||
item = {**cell_input("spend", 320.0), "artifact": snap_id}
|
||||
card = card_with([item], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
|
||||
_, entry = bundle_of(card, files_changed={snap_id: raw})
|
||||
assert "masked" not in entry and base64.b64decode(entry["b64"]) == stored(card)[1]
|
||||
|
||||
|
||||
def test_an_error_while_finding_renamed_columns_masks_the_whole_snapshot(monkeypatch):
|
||||
"""按 SQL 认改了名字的遮罩列时出了意外:认不出哪几列取自遮罩列,整份快照的列都按遮罩算,卡上取消费额也只给 id。"""
|
||||
from app.engine import inquiry
|
||||
|
||||
def broken(*_args, **_kwargs):
|
||||
raise RuntimeError("解析器坏了")
|
||||
|
||||
monkeypatch.setattr(inquiry, "_derived_hidden", broken)
|
||||
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
|
||||
_, entry = bundle_of(card)
|
||||
assert entry.get("masked") is True and "b64" not in entry
|
||||
|
||||
|
||||
def test_a_mask_set_on_the_source_now_counts_too():
|
||||
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
|
||||
_, entry = bundle_of(card, source_masks={"crm": ["spend"]})
|
||||
|
||||
Reference in New Issue
Block a user