Files
AgentLab/backend/tests/test_bundle_masked_cards.py
T
ThearchyHelios b080302e77 可验证报告复核修正(第 1 轮):只给 id 的遮罩工件,服务端的文件和 id 对不上的标 mismatch、解析不出来的标 unparsable,核对页对不上的判不通过、解析不出来的照实说认不出是否含遮罩列
上一轮让被改过、解析不出的口径卡不再原样嵌入(号码不进文件),可这样一来它在页面上和正常的遮罩卡一模一样:「? 无法核对 含遮罩列,未随文件附带」,总结论从「有 1 项不通过」变成「可核对的项全部通过」,篡改的证据丢了;截断的卡服务端其实认不出输入,却被说成「含遮罩列」。现在 _Artifacts.add 对只给 id 的遮罩工件(口径卡、查询快照都算,带遮罩的快照被改过原来就有同样的问题)照实带上服务端看到的:store.intact 为假标 mismatch,解析不出对象标 unparsable。verifier.js 在遮罩分支认这两个标记:mismatch 判不通过,原因「导出时服务端的这份文件与工件 id(哈希)不一致,可能被改过;按含遮罩列处理,未随文件附带」;unparsable 是正常的无法核对,原因「导出时服务端的这份文件无法解析,认不出是否含遮罩列,按含遮罩列处理,未随文件附带」,引用它的数字跟着用这句。pytest 补被改过、截断、不是对象、哈希对得上却解析不出的卡和被改过的遮罩快照;check-bundle 在 masked-card 样本上补两条篡改检查。
2026-10-06 13:43:04 +08:00

180 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""可验证报告里的口径卡和遮罩(期 4 I2a 复核):口径卡上记着每个输入的值和代入式,有输入取自遮罩列时整张卡只给 id。
以前只有查询快照会因遮罩不随文件附带,口径卡一律原样嵌入:证据面板上已经遮住的号码(cell() 取的遮罩格、按位置取的
vars.res.rows[0][1]),在这份要发给别人的文件里照样看得到。认哪个输入取自哪份查询结果的哪几列,和证据面板、推演同一个
认法(recompute.input_origin);核对不了的宁可多挡。
服务端的卡文件被改过(哈希对不上)也照卡上记的输入认:嵌进去的是原字节,号码照样在里面。读得出字节、却解析不出一张卡
(截断的 JSON、不是对象)的,认不出输入,整张按带遮罩算、不嵌:和读不出来的快照按带遮罩算是同一个道理。
不嵌的工件,服务端把它知道的情况照实带上:自己的文件和工件 id(哈希)对不上的标 mismatch,解析不出来的标 unparsable,
核对页据此判不通过、照实说原因(以前原字节嵌进去,浏览器自己算出对不上;现在不嵌了,这个证据不能跟着丢)。带遮罩的
查询快照同样标。
工件、事件都是手搭的(会员、号码都是编的),照 tests/test_bundle_build.py 的样子。
"""
from __future__ import annotations
import base64
import pytest
from app.bundle.build import build_bundle, render_html
from app.core.artifact_store import content_hash
from tests.test_bundle_build import GRAPH, MASKED_ID, RUN, SNAP_MASKED, STORE, doc_of, stored
#: SNAP_MASKED 里的号码(phone 设了遮罩)
PHONE = SNAP_MASKED["rows"][0][1]
CELL = "cell(nodes.members, 0, 'phone')"
def card_with(inputs, substituted="len('000-0001')", expression=f"len({CELL})"):
"""会员号码位数一张卡:报告引用的是它(m:gmv 指过来)。"""
return {"kind": "metric_set", "caliber": "会员口径", "caliber_version": "v1", "metrics": [
{"id": "gmv", "name": "号码位数", "value": 1280.5, "unit": "元", "decimals": 1, "expression": expression,
"substituted": substituted, "inputs": inputs}]}
def bundle_of(card, *, outputs=None, source_masks=None, tamper=None, rehash=False, files_changed=None):
"""一次手搭的运行:会员查询(带遮罩的快照)→ 数据整形(outputs 里的产出)→ 口径卡 → 报告。tamper 给了就把服务端的
卡文件换成 tamper(原字节),id 照旧(文件被改过、哈希对不上);rehash 为真时 id 按换过的字节算(服务端存的就是这一份,
哈希对得上)。files_changed 换掉服务端别的文件({工件 id: 新字节},id 照旧)。"""
card_id, card_raw = stored(card)
if tamper is not None:
card_raw = tamper(card_raw)
if rehash:
card_id = content_hash(card_raw)
doc = doc_of(extra_catalog={"m:gmv": {"alias": "m:gmv", "kind": "metric", "artifact": card_id, "node_id": "card",
"locator": {"metric": "gmv"}, "value": 1280.5}})
doc_id, doc_raw = stored(doc)
files = {**STORE, card_id: card_raw, doc_id: doc_raw, **(files_changed or {})}
events = [
(1, "run.started", None, {"nodes": 7}),
(2, "tool.end", "members", {"tool": "db_query__crm", "query_artifact": MASKED_ID}),
(3, "node.finished", "members", {"evidence": [{"kind": "query", "node_id": "members", "artifact": MASKED_ID}]}),
]
for node_id, content in (outputs or {}).items():
artifact, raw = stored(content) if content is not None else ("ee" * 32, None)
if raw is not None:
files[artifact] = raw
events.append((len(events) + 1, "node.finished", node_id, {"artifact": artifact}))
events += [
(len(events) + 1, "node.started", "card", {}),
(len(events) + 2, "node.finished", "card", {"evidence": [{"kind": "metric_set", "node_id": "card",
"artifact": card_id}]}),
(len(events) + 3, "report.checked", "write", {"doc_artifact": doc_id, "ok": True}),
(len(events) + 4, "run.finished", None, {"output": {}}),
]
bundle = build_bundle(RUN, events, manifest_seq=len(events), manifest_hash="0" * 64, graph=GRAPH,
source_masks=source_masks or {}, read=files.get, exported_at="2026-10-06T08:00:00+00:00")
entry = next(a for a in bundle["artifacts"] if a["id"] == card_id)
return bundle, entry
def cell_input(column, value):
return {"path": f"cell(nodes.members, 0, '{column}')", "value": value, "node_id": "members", "via": "tool_cell",
"artifact": MASKED_ID, "locator": {"row": 0, "column": column}, "status": "verified"}
def test_a_card_reading_a_masked_cell_ships_only_its_id():
bundle, entry = bundle_of(card_with([cell_input("phone", PHONE)]))
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True}
assert PHONE not in render_html(bundle)
def test_a_card_reading_only_unmasked_columns_is_embedded():
"""同一份快照里没遮的列(消费额):卡上没有遮罩列的值,照常嵌入、照常核对。"""
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
_, entry = bundle_of(card)
assert "masked" not in entry and base64.b64decode(entry["b64"]) == stored(card)[1]
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"]})
assert entry.get("masked") is True and "b64" not in entry
PARSED = {"columns": SNAP_MASKED["columns"], "rows": SNAP_MASKED["rows"]}
POSITIONAL = "vars.res.rows[0][1]"
@pytest.mark.parametrize("parsed, masked", [
# 数据整形原样解析的查询结果(带着快照的 id):认得出取自 phone 列
({**PARSED, "artifact": MASKED_ID, "source": "crm"}, True),
# 写变量的节点的产出读不出来:核对不了,宁可多挡
(None, True),
# 结果没存快照、只带着数据源名:查询当时记下的遮罩无从核对,宁可多挡
({**PARSED, "source": "crm"}, True),
# 改过形状、既没带快照的 id 也没带数据源名:认不出出处,照常嵌入(和证据面板、推演一样,是已知的缺口)
(PARSED, False),
])
def test_a_card_reading_a_cell_by_position_is_judged_the_same_way(parsed, masked):
item = {"path": POSITIONAL, "value": PHONE, "node_id": "parse", "via": "transform", "field": "rows[0][1]",
"status": "ok"}
card = card_with([item], substituted=f"len('{PHONE}')", expression=f"len({POSITIONAL})")
bundle, entry = bundle_of(card, outputs={"parse": parsed})
assert (entry.get("masked") is True) is masked and ("b64" in entry) is not masked
if masked:
assert PHONE not in render_html(bundle)
else:
assert PHONE in base64.b64decode(entry["b64"]).decode()
def test_a_card_file_changed_on_the_server_is_still_judged_by_its_inputs():
"""服务端的卡文件被改过(这里改了口径版本,哈希对不上):照样按卡上记的输入认遮罩,记着号码的只给 id。以前先看哈希、
对不上就当卡上没有输入,原字节照样嵌进页面,号码进了文件。不嵌了,就另标 mismatch:核对页判这一项不通过,不和
没被改过的遮罩卡一样说成正常的无法核对。"""
card = card_with([cell_input("phone", PHONE)])
bundle, entry = bundle_of(card, tamper=lambda raw: raw.replace(b'"caliber_version":"v1"', b'"caliber_version":"v9"'))
assert entry["id"] == stored(card)[0]
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True, "mismatch": True}
assert PHONE not in render_html(bundle)
def test_a_changed_card_without_masked_inputs_is_still_embedded_for_the_browser_to_judge():
"""改过的卡上没有遮罩列的值:照常原样嵌进去,浏览器自己算出哈希对不上(只是遮罩的判断不再看哈希)。"""
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
changed = stored(card)[1].replace(b'"caliber_version":"v1"', b'"caliber_version":"v9"')
_, entry = bundle_of(card, tamper=lambda raw: changed)
assert "masked" not in entry and base64.b64decode(entry["b64"]) == changed
@pytest.mark.parametrize("damage", [
# 截断的 JSON:号码还在已经写下的那半截里
lambda raw: raw[: raw.index(b"000-0001") + len(b"000-0001")],
# 解析得出、却不是一张卡(对象)
lambda raw: b'["000-0001"]',
])
def test_a_card_file_that_does_not_parse_as_a_card_is_not_embedded(damage):
"""服务端的卡文件读得出字节、解析不出一张卡:认不出卡上有哪些输入,整张按带遮罩算、只给 id,不原样嵌进页面。字节和
id 对不上(mismatch),也解析不出来(unparsable):两样都照实标上。"""
card = card_with([cell_input("phone", PHONE)])
bundle, entry = bundle_of(card, tamper=damage)
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True, "mismatch": True,
"unparsable": True}
assert PHONE not in render_html(bundle)
def test_a_card_stored_as_something_unparsable_is_marked_so():
"""服务端存的这一份本来就解析不出一张卡(哈希对得上,不是被改过):整张按带遮罩算、不嵌,只标 unparsable——核对页照实
说认不出是否含遮罩列,不说成「含遮罩列」,也不说被改过。"""
card = card_with([cell_input("phone", PHONE)])
bundle, entry = bundle_of(card, tamper=lambda raw: raw[: raw.index(b"000-0001") + len(b"000-0001")], rehash=True)
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True, "unparsable": True}
assert PHONE not in render_html(bundle)
def test_a_masked_snapshot_changed_on_the_server_is_marked_so():
"""带遮罩的查询快照在服务端被改过(哈希对不上):照旧只给 id,另标 mismatch,核对页判不通过。以前和没被改过的遮罩快照
一模一样,总结论还是「可核对的项全部通过」。没被改过的照旧只标遮罩。"""
card = card_with([cell_input("spend", 320.0)], substituted="320.0", expression="cell(nodes.members, 0, 'spend')")
snapshot = lambda bundle: next(a for a in bundle["artifacts"] if a["id"] == MASKED_ID) # noqa: E731
bundle, _ = bundle_of(card)
assert snapshot(bundle) == {"id": MASKED_ID, "kind": "query", "node_id": "members", "masked": True}
changed = stored(SNAP_MASKED)[1].replace("林一".encode(), "林二".encode())
bundle, _ = bundle_of(card, files_changed={MASKED_ID: changed})
assert snapshot(bundle) == {"id": MASKED_ID, "kind": "query", "node_id": "members", "masked": True,
"mismatch": True}
assert PHONE not in render_html(bundle)