From 8ede152b6f4b944cd55dbedce06cb408428db49c Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 17:11:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E9=AA=8C=E8=AF=81=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=EF=BC=9A=E5=8F=A3=E5=BE=84=E5=8D=A1=E5=8F=96=E7=9A=84=E5=88=97?= =?UTF-8?q?=E6=98=AF=20SQL=20=E9=87=8C=E6=94=B9=E4=BA=86=E5=90=8D=E5=AD=97?= =?UTF-8?q?=E7=9A=84=E9=81=AE=E7=BD=A9=E5=88=97=E6=97=B6=EF=BC=8C=E6=95=B4?= =?UTF-8?q?=E5=BC=A0=E5=8D=A1=E5=90=8C=E6=A0=B7=E5=8F=AA=E7=BB=99=E5=93=88?= =?UTF-8?q?=E5=B8=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 口径卡上记着每个输入的值和代入式,有输入取自遮罩列的整张卡只给哈希;但以前只按列名认:查询写 phone AS tel、经子查询 或 WITH 转一道、套个函数,卡上取 tel 那一格就是号码,卡照样原样嵌进要发给别人的文件。现在那几列算不算遮罩列另按 SQL 认改了名字的(_Store.hidden → _derived_columns,用 inquiry._derived_hidden,和质询、双盲复算同一个认法;SQL 按每种 读得通的读法各认一遍合起来,都读不通的写着遮罩列就整份的列都算,出错时整份的列都算)。同一条查询里没取自遮罩列的 列照常嵌入。新测试覆盖别名、子查询、WITH 加函数三种改名,以及没改名的列、认的过程出错;每处都做过去掉实现测试变红的 验证。 --- backend/app/bundle/build.py | 40 ++++++++++++++++++--- backend/tests/test_bundle_masked_cards.py | 43 ++++++++++++++++++++++- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/backend/app/bundle/build.py b/backend/app/bundle/build.py index 4b380fd9..0c4a3672 100644 --- a/backend/app/bundle/build.py +++ b/backend/app/bundle/build.py @@ -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"(?