mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
可验证报告复核修正(第 1 轮):只给 id 的遮罩工件,服务端的文件和 id 对不上的标 mismatch、解析不出来的标 unparsable,核对页对不上的判不通过、解析不出来的照实说认不出是否含遮罩列
上一轮让被改过、解析不出的口径卡不再原样嵌入(号码不进文件),可这样一来它在页面上和正常的遮罩卡一模一样:「? 无法核对 含遮罩列,未随文件附带」,总结论从「有 1 项不通过」变成「可核对的项全部通过」,篡改的证据丢了;截断的卡服务端其实认不出输入,却被说成「含遮罩列」。现在 _Artifacts.add 对只给 id 的遮罩工件(口径卡、查询快照都算,带遮罩的快照被改过原来就有同样的问题)照实带上服务端看到的:store.intact 为假标 mismatch,解析不出对象标 unparsable。verifier.js 在遮罩分支认这两个标记:mismatch 判不通过,原因「导出时服务端的这份文件与工件 id(哈希)不一致,可能被改过;按含遮罩列处理,未随文件附带」;unparsable 是正常的无法核对,原因「导出时服务端的这份文件无法解析,认不出是否含遮罩列,按含遮罩列处理,未随文件附带」,引用它的数字跟着用这句。pytest 补被改过、截断、不是对象、哈希对得上却解析不出的卡和被改过的遮罩快照;check-bundle 在 masked-card 样本上补两条篡改检查。
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
base64 嵌入,浏览器重算 sha256 和 id 比;带遮罩的快照不嵌内容、只给 id。口径卡上记着每个输入的值和代入式,
|
||||
有输入取自遮罩列(cell() 的格、Agent 字段、按位置取的 vars.res.rows[0][2])的整张卡同样只给 id,认法和证据
|
||||
面板、推演同一个(recompute.input_origin);服务端文件被改过的卡照样按卡上记的输入认,解析不出一张卡的整张只给
|
||||
id。没被报告引用的工件不嵌。
|
||||
id。只给 id 的工件,服务端的文件和 id 对不上的另标 mismatch、解析不出来的另标 unparsable,核对页照实说(对不上的
|
||||
判不通过)。没被报告引用的工件不嵌。
|
||||
- 封存之后追加的事件(按需裁判、审批质询这类批注)单列:类型、序号、内容摘要,不进清单。
|
||||
|
||||
它只证明「内部一致」:数字、文档、运行记录彼此对得上,而且没被改过。真伪要拿页面上的封存哈希和系统里这次
|
||||
@@ -337,6 +338,12 @@ class _Artifacts:
|
||||
elif (kind == "query" and self.store.masked(artifact)) \
|
||||
or (kind == "metric_set" and self.store.card_masked(artifact, self.events)):
|
||||
item["masked"] = True
|
||||
# 不嵌内容,浏览器就算不出这份文件对不对得上:服务端自己看到的照实带上,核对页据此判不通过、说清原因。
|
||||
# 以前被改过的卡原样嵌进去,浏览器自己算出对不上;现在不嵌了,这个证据不能跟着丢
|
||||
if not self.store.intact(artifact):
|
||||
item["mismatch"] = True
|
||||
if not isinstance(self.store.content(artifact), dict):
|
||||
item["unparsable"] = True
|
||||
else:
|
||||
# 原样嵌入,不在这里复验哈希:被改过的文件照实交出去,浏览器自己算出对不上
|
||||
item["b64"] = base64.b64encode(bytes(raw)).decode("ascii")
|
||||
|
||||
@@ -106,6 +106,8 @@
|
||||
artifactUndecodable: '内容无法解码',
|
||||
artifactUnparsable: '哈希一致,但内容无法解析',
|
||||
masked: '含遮罩列,未随文件附带,只能核对到哈希',
|
||||
maskedMismatch: '导出时服务端的这份文件与工件 id(哈希)不一致,可能被改过;按含遮罩列处理,未随文件附带',
|
||||
maskedUnparsable: '导出时服务端的这份文件无法解析,认不出是否含遮罩列,按含遮罩列处理,未随文件附带',
|
||||
maskedOdd: '只有查询快照和口径卡会因遮罩不随文件附带,这份工件却标着遮罩:导出时不会出现这种情况,文件可能被改过',
|
||||
artifactMissing: '导出时服务端已没有这份工件的文件',
|
||||
artifactAbsent: '内容未随文件附带:导出时不会出现这种情况,文件可能被改过',
|
||||
@@ -452,7 +454,7 @@
|
||||
out.manifest.push({ kind: 'shown', shown: shownSeal, result: shownSeal === seal ? PASS : FAIL,
|
||||
reason: shownSeal === seal ? T.shownOk : T.shownMismatch })
|
||||
|
||||
// ③ 工件。服务端给每份工件的是三样之一:原字节、遮罩(只有查询快照)、服务端已没有文件
|
||||
// ③ 工件。服务端给每份工件的是三样之一:原字节、遮罩(查询快照、口径卡,可另带 mismatch、unparsable)、服务端已没有文件
|
||||
const arts = new Map()
|
||||
for (const a of Array.isArray(data.artifacts) ? data.artifacts.filter(isObj) : []) {
|
||||
const item = { a, result: NA, reason: '', json: null, loose: null, parsed: false }
|
||||
@@ -485,12 +487,17 @@
|
||||
}
|
||||
}
|
||||
} else if (a.masked) {
|
||||
// 查询快照带遮罩列;口径卡上有输入取自遮罩列(卡上记着输入的值和代入式)。两种都只给 id,报告文档不会
|
||||
if (a.kind === 'query' || a.kind === 'metric_set') {
|
||||
item.reason = T.masked
|
||||
} else {
|
||||
// 查询快照带遮罩列;口径卡上有输入取自遮罩列(卡上记着输入的值和代入式)。两种都只给 id,报告文档不会。
|
||||
// 内容不随文件附带,浏览器算不出它对不对得上:导出时服务端看到自己的文件和 id 对不上(mismatch)、解析不出来
|
||||
// (unparsable)的照实说,对不上的判不通过——不然被改过的文件看起来和正常的遮罩一样,篡改的证据就丢了
|
||||
if (a.kind !== 'query' && a.kind !== 'metric_set') {
|
||||
item.result = FAIL
|
||||
item.reason = T.maskedOdd
|
||||
} else if (a.mismatch) {
|
||||
item.result = FAIL
|
||||
item.reason = T.maskedMismatch
|
||||
} else {
|
||||
item.reason = a.unparsable ? T.maskedUnparsable : T.masked
|
||||
}
|
||||
} else if (a.missing) {
|
||||
item.reason = T.artifactMissing
|
||||
@@ -617,7 +624,7 @@
|
||||
if (!art) return { ...info, result: FAIL, reason: T.numArtifactNotListed }
|
||||
if (art.result === FAIL) return { ...info, result: NA, reason: T.numArtifactFailed }
|
||||
if (art.result !== PASS) {
|
||||
return { ...info, result: NA, reason: art.a.masked ? T.masked : art.a.missing ? T.artifactMissing : T.numArtifactUnparsable }
|
||||
return { ...info, result: NA, reason: art.a.masked ? art.reason : art.a.missing ? T.artifactMissing : T.numArtifactUnparsable }
|
||||
}
|
||||
if (!chained.has(entry.artifact)) return { ...info, result: NA, reason: T.numNotChained }
|
||||
if (cite.kind === 'metric') {
|
||||
|
||||
@@ -7,6 +7,10 @@ vars.res.rows[0][1]),在这份要发给别人的文件里照样看得到。
|
||||
服务端的卡文件被改过(哈希对不上)也照卡上记的输入认:嵌进去的是原字节,号码照样在里面。读得出字节、却解析不出一张卡
|
||||
(截断的 JSON、不是对象)的,认不出输入,整张按带遮罩算、不嵌:和读不出来的快照按带遮罩算是同一个道理。
|
||||
|
||||
不嵌的工件,服务端把它知道的情况照实带上:自己的文件和工件 id(哈希)对不上的标 mismatch,解析不出来的标 unparsable,
|
||||
核对页据此判不通过、照实说原因(以前原字节嵌进去,浏览器自己算出对不上;现在不嵌了,这个证据不能跟着丢)。带遮罩的
|
||||
查询快照同样标。
|
||||
|
||||
工件、事件都是手搭的(会员、号码都是编的),照 tests/test_bundle_build.py 的样子。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -16,6 +20,7 @@ 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 设了遮罩)
|
||||
@@ -30,16 +35,19 @@ def card_with(inputs, substituted="len('000-0001')", expression=f"len({CELL})"):
|
||||
"substituted": substituted, "inputs": inputs}]}
|
||||
|
||||
|
||||
def bundle_of(card, *, outputs=None, source_masks=None, tamper=None):
|
||||
def bundle_of(card, *, outputs=None, source_masks=None, tamper=None, rehash=False, files_changed=None):
|
||||
"""一次手搭的运行:会员查询(带遮罩的快照)→ 数据整形(outputs 里的产出)→ 口径卡 → 报告。tamper 给了就把服务端的
|
||||
卡文件换成 tamper(原字节),id 照旧(文件被改过、哈希对不上)。"""
|
||||
卡文件换成 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 = {**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}),
|
||||
@@ -115,11 +123,12 @@ def test_a_card_reading_a_cell_by_position_is_judged_the_same_way(parsed, masked
|
||||
|
||||
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}
|
||||
assert entry == {"id": entry["id"], "kind": "metric_set", "node_id": "card", "masked": True, "mismatch": True}
|
||||
assert PHONE not in render_html(bundle)
|
||||
|
||||
|
||||
@@ -138,8 +147,33 @@ def test_a_changed_card_without_masked_inputs_is_still_embedded_for_the_browser_
|
||||
lambda raw: b'["000-0001"]',
|
||||
])
|
||||
def test_a_card_file_that_does_not_parse_as_a_card_is_not_embedded(damage):
|
||||
"""服务端的卡文件读得出字节、解析不出一张卡:认不出卡上有哪些输入,整张按带遮罩算、只给 id,不原样嵌进页面。"""
|
||||
"""服务端的卡文件读得出字节、解析不出一张卡:认不出卡上有哪些输入,整张按带遮罩算、只给 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}
|
||||
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)
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
// 经继续运行跑到成功的早期运行、两条出具
|
||||
// 判定、服务端缺文件、裸 NaN、别的运行的文档、口径卡有输入取自遮罩列),逐份断言各项结果和原因文案;在上面篡改 ④
|
||||
// 引用链(换文档、拿掉报告、多一份没人引用的工件)、⑥ 运行信息(运行结果、类别、出具档位)、拿掉被引用的口径卡、把
|
||||
// 报告文档标成遮罩、只改页面顶部的封存哈希;宽屏点开表格里右对齐的数字不被面板盖住;不执行脚本时封存哈希、运行 ID 照样看得到,状态写「尚未核对」;
|
||||
// 报告文档标成遮罩、把遮罩卡标成服务端的文件与 id 不一致或解析不出来(服务端对被改过、解析不了的遮罩卡就这么标)、
|
||||
// 只改页面顶部的封存哈希;宽屏点开表格里右对齐的数字不被面板盖住;不执行脚本时封存哈希、运行 ID 照样看得到,状态写
|
||||
// 「尚未核对」;
|
||||
// - 记录页的导出按钮(预览用例 ?syn=bundle-provenance-*,page.route 假装导出接口):已封存时可点、下载的文件名同服务端;
|
||||
// 未封存时不可点并说明原因(左边写着「已封存」却不能点时原因写在屏幕上);服务端回 409 时提示原因;失败结束的运行
|
||||
// 提示之后继续运行会重新封存。
|
||||
@@ -675,6 +677,39 @@ await section('样本页:篡改', async () => {
|
||||
doc.masked = true
|
||||
return '报告文档去掉内容、标遮罩'
|
||||
}, (s) => [...turnsRed('artifacts')(s), ['原因写明', hasItem(s, 'artifacts', 'fail', '只有查询快照和口径卡会因遮罩不随文件附带')]])
|
||||
// 遮罩卡在服务端被改过时,服务端照样只给 id、另标 mismatch(tests/test_bundle_masked_cards.py 守着服务端这一边):
|
||||
// 不能因为不嵌内容就和正常的遮罩卡一样「可核对的项全部通过」
|
||||
await tamper(samples['masked-card'], '遮罩卡标成服务端的文件与 id 不一致', (data) => {
|
||||
byKind(data, 'metric_set').mismatch = true
|
||||
return '口径卡加 mismatch'
|
||||
}, (s) => {
|
||||
const card = s.items.find((i) => i.check === 'artifacts' && i.text.includes('口径卡'))
|
||||
const metricNumber = s.items.find((i) => i.check === 'numbers' && i.text.includes('销售额'))
|
||||
return [
|
||||
...turnsRed('artifacts', ['events', 'manifest', 'chain'])(s),
|
||||
['口径卡写明导出时服务端的文件与 id 不一致、可能被改过、未随文件附带', card?.result === 'fail'
|
||||
&& card.text.includes('导出时服务端的这份文件与工件 id(哈希)不一致,可能被改过') && card.text.includes('未随文件附带'),
|
||||
card?.text],
|
||||
['引用这张卡的数字无法核对,写明引用的工件没有通过核对', metricNumber?.result === 'na'
|
||||
&& metricNumber.text.includes('引用的工件没有通过核对'), metricNumber?.text],
|
||||
]
|
||||
})
|
||||
// 服务端标了解析不出来、哈希又对得上的遮罩卡:照实说认不出是否含遮罩列,是正常的无法核对(同哈希一致却解析不了的
|
||||
// 嵌入工件),不说成「含遮罩列」
|
||||
await tamper(samples['masked-card'], '遮罩卡标成服务端解析不出来', (data) => {
|
||||
byKind(data, 'metric_set').unparsable = true
|
||||
return '口径卡加 unparsable'
|
||||
}, (s) => {
|
||||
const card = s.items.find((i) => i.check === 'artifacts' && i.text.includes('口径卡'))
|
||||
const metricNumber = s.items.find((i) => i.check === 'numbers' && i.text.includes('销售额'))
|
||||
return [
|
||||
['口径卡无法核对,写明服务端的文件无法解析、认不出是否含遮罩列', card?.result === 'na'
|
||||
&& card.text.includes('导出时服务端的这份文件无法解析,认不出是否含遮罩列'), card?.text],
|
||||
['引用它的数字原因同上', metricNumber?.result === 'na' && metricNumber.text.includes('认不出是否含遮罩列'),
|
||||
metricNumber?.text],
|
||||
['总览照常通过', s.overall === 'pass', `${s.overall}:${s.text}`],
|
||||
]
|
||||
})
|
||||
await tamper(base, '报告文档的一个字节', (data) => flipByte(byKind(data, 'report_doc')), (s) => [
|
||||
...turnsRed('artifacts')(s),
|
||||
['数字不予核对', s.checks.numbers === 'na' && hasItem(s, 'numbers', 'na', '所在的报告文档没有通过核对')],
|
||||
|
||||
Reference in New Issue
Block a user