mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
K3b 复核修正(第 3 轮):VALUES 当一张表用又读到遮罩列的整份遮,列名按方言认;sys 库读 performance_schema 的 ps_* 函数进认不全的名单
- VALUES(复核 major,基线就有):_function_columns 按 Postgres、SQLite 的习惯把 VALUES 的列记成 column1…,MySQL 8.0 实际叫 column_0、column_1…(真库实测),派生表、CTE、LATERAL 里的 VALUES ROW 取了号码,外层按名字取、照原名带出时 对不上,号码原样交给模型、落库;MariaDB 拿第一行的值当列名,同样对不上。现在 VALUES 当一张表用(FROM、JOIN、 LATERAL 里的,WITH 里的一段,UNION 的一支,整条就是它,_values_table)又读到遮罩列(含派生出来的名字)的,不论哪个 库、列怎么起名,整份遮。套了括号的 VALUES(整条、UNION 的一支)解析器原样挂在树上、不在任何 SELECT 的 FROM 里,以前 哪个库都没认(Postgres 也漏),一并收口。IN / ANY / EXISTS、标量子查询里的 VALUES 不算:值经外层的表达式带出,或者 只用来筛行 - 列名按方言认(_values_names,_function_columns 用同一套):Postgres、SQLite 叫 column1…,MySQL 叫 column_0…(一行 ROW(…) 摊开数列),写了列名表的按列名表(Postgres 的列名表只改前几列),MariaDB、Oracle 认不准记 None。整份遮时只收 值取自遮罩列的那几列的值(_values_taken 按位置):以前 VALUES 按表值函数认,出的每一列(连同 key、value、别名)都算, 同一行里没碰遮罩列的区域收成遮罩值,在同一次核查里处处被换掉 - 行为变化:Postgres、SQLite 的 LATERAL VALUES 读到遮罩列,以前只遮那几列,现在整份遮(多遮,同一份里的编号也遮); 原来「VALUES 里直接写遮罩列只遮那一列」的用例改成整份遮 - sys 库(复核 minor):INDIRECT_NAMES 的 sys 规则补上按 sys.ps_ 开头认的函数(ps_thread_trx_info 交回线程的事务里执行 过的语句原文,ps_thread_stack 同类;只交回编号、开关的 ps_thread_id、ps_is_* 一并算,多遮;不写 sys. 的要默认库就是 sys 才执行得了,不认,同 sys 的视图)。工具边界和可验证报告共用这一份名单,报告不再判它没碰遮罩列、照常附带快照 - 质询模块说明的已知局限同步 补测试:VALUES 当一张表用 × MySQL(派生表、LATERAL、CTE、外层改名、JOIN LATERAL、LATERAL 子查询、标量子查询、派生 出来的名字、UNION 第一支、整条套括号、派生表里 UNION 的一支)、MariaDB 两种、Postgres 三种、SQLite 一种;三种没读到 遮罩列的对照;四种写法的收值(号码收、区域不收)和 (表达式).* 之后不按位置收;_function_columns 四种列名、两种认不准; 核查 agent 真实查询工具上的端到端(复核点名的三条 MySQL 写法);sys.ps_* 六种写法、三种不该误伤的对照、可验证报告 两种、端到端一条。逐项去掉实现(整份遮、MySQL 的起名、按位置收值、当一张表用的位置、整条就是 VALUES、VALUES 不按表值 函数认、摊开 ROW、列名表、_function_columns 的起名、(表达式).* 之后停、MariaDB / Oracle 认不准、sys.ps_),对应的测试 都变红。另在 MySQL 8.0.46、Postgres 18 临时实例上经核查 agent 的真实查询工具复跑 28 条(MySQL 的 VALUES 14 条、对照 4 条、sys 函数 4 条,Postgres 的 VALUES 6 条):交给模型的、落库的、通用工件接口给的都没有号码;2b5989e 上其中 16 条漏 号码(sys.ps_thread_stack 在只读事务里执行不了,前后一样)。
This commit is contained in:
@@ -61,9 +61,9 @@
|
||||
复算),反方审稿核对这类汇总数的结论也拿不到数。汇总里套着结果全是写死常量的 CASE(SUM(CASE WHEN amount <= 20000
|
||||
THEN 1 ELSE 0 END))只是按条件数个数,同 COUNT;汇总里的 CASE 只在条件里用到遮罩列的(SUM(CASE WHEN buyer_phone
|
||||
IS NOT NULL THEN amount ELSE 0 END))同写在 WHERE 里,不算
|
||||
- FROM、JOIN、LATERAL 里的表值函数(json_each、json_tree、unnest、jsonb_each_text、JSON_TABLE、LATERAL VALUES)拆开
|
||||
遮罩列的,按出列名认它出的列(常见函数的出列名、写的列名表、别名、函数名,_function_columns),同名的别的列一并遮
|
||||
(多遮);不认得的函数又没写列名表的,整份快照都遮。Postgres 里可能交回复合类型、展开成各个字段的也整份遮
|
||||
- FROM、JOIN、LATERAL 里的表值函数(json_each、json_tree、unnest、jsonb_each_text、JSON_TABLE)拆开遮罩列的,按
|
||||
出列名认它出的列(常见函数的出列名、写的列名表、别名、函数名,_function_columns),同名的别的列一并遮(多遮);
|
||||
不认得的函数又没写列名表的,整份快照都遮。Postgres 里可能交回复合类型、展开成各个字段的也整份遮
|
||||
(masking._spreads_record):unnest(数组的元素是复合类型时展开成 addr、tel 这些字段,写了列名表也只改前几列的
|
||||
名字),不认得的函数只写了列名、没写类型的列名表(jsonb_populate_record(NULL::contact, …) AS r(a) 后面的字段
|
||||
照原名出);拆的是普通数组的 unnest 也一样整份遮(元素是什么类型 SQL 里看不出,多遮)。函数参数里没写表名的
|
||||
@@ -90,15 +90,18 @@
|
||||
按看不见算(当成数据源里的表,多遮)
|
||||
- 认不全的写法整份遮(多遮,同一份里没碰遮罩列的编号、金额也遮):(表达式).* 展开读到遮罩列的(复合类型的列
|
||||
(p.profile).*、函数交回的记录 (jsonb_each_text(pj)).*、子查询里的 ROW (q.r).*,外面套了括号、起了别名的也算,展开成
|
||||
几列、各叫什么 SQL 里看不出;不只选出项,VALUES 里的、行构造器和函数参数里的也算),VALUES 里的 s.*、(s.*) 读到遮罩列
|
||||
的(同样展开成好几列);语法树和实际执行的对不上的(MySQL、MariaDB 会执行的注释 /*! … */,Postgres 的
|
||||
U&"…" 名字,没有 * 的一支选出项和结果的列数对不上);表名、SQL 写在字符串里交给函数执行、整表导出的(query_to_xml、
|
||||
table_to_xml、cursor_to_xml、schema_to_xml、database_to_xml 这一族,dblink、crosstab、ts_stat,Oracle 的
|
||||
DBMS_XMLGEN、XMLTYPE)、按页按文件按日志读原始数据的(pageinspect、逻辑解码、LOAD_FILE)、统计信息、会话记录、锁和
|
||||
日志里存着值的(Postgres 的 pg_stats、pg_stat_statements、pg_stat_activity;SQLite 的 sqlite_stat4;MySQL 的
|
||||
column_statistics、general_log、PROCESSLIST、INNODB_TRX、OPTIMIZER_TRACE、performance_schema;MariaDB 的
|
||||
column_stats、INNODB_LOCKS、QUERY_CACHE_INFO;Oracle 的直方图、记着最小最大值的 *_tab_columns、V$SQL 一族、
|
||||
SQL 调优集、固定下来的执行计划、闪回事务、LogMiner、审计记录),按名字认(masking 的 INDIRECT_FUNCTIONS、
|
||||
几列、各叫什么 SQL 里看不出;不只选出项,VALUES 里的、行构造器和函数参数里的也算),VALUES 当一张表用(FROM、JOIN、
|
||||
LATERAL 里的,WITH 里的一段,UNION 的一支)又读到遮罩列的(直接写遮罩列、经子查询或派生来源改了名字的、里面写着
|
||||
s.*、(s.*) 的都算;各库给 VALUES 的列起名不一样:Postgres、SQLite 叫 column1…,MySQL 叫 column_0…,MariaDB 拿第一行
|
||||
的值当列名,不逐个认,同一份里的编号、区域也遮,只收值取自遮罩列的那几列的值);语法树和实际执行的对不上的(MySQL、
|
||||
MariaDB 会执行的注释 /*! … */,Postgres 的 U&"…" 名字,没有 * 的一支选出项和结果的列数对不上);表名、SQL 写在字符串里
|
||||
交给函数执行、整表导出的(query_to_xml、table_to_xml、cursor_to_xml、schema_to_xml、database_to_xml 这一族,dblink、
|
||||
crosstab、ts_stat,Oracle 的 DBMS_XMLGEN、XMLTYPE)、按页按文件按日志读原始数据的(pageinspect、逻辑解码、
|
||||
LOAD_FILE)、统计信息、会话记录、锁和日志里存着值的(Postgres 的 pg_stats、pg_stat_statements、pg_stat_activity;
|
||||
SQLite 的 sqlite_stat4;MySQL 的 column_statistics、general_log、PROCESSLIST、INNODB_TRX、OPTIMIZER_TRACE、
|
||||
performance_schema,sys 库里读它的函数(按 sys.ps_ 开头认:ps_thread_trx_info、ps_thread_stack 交回别的线程执行过的
|
||||
语句原文);MariaDB 的 column_stats、INNODB_LOCKS、QUERY_CACHE_INFO;Oracle 的直方图、记着最小最大值的 *_tab_columns、
|
||||
V$SQL 一族、SQL 调优集、固定下来的执行计划、闪回事务、LogMiner、审计记录),按名字认(masking 的 INDIRECT_FUNCTIONS、
|
||||
INDIRECT_NAMES,可验证报告用的同一份名单),去掉注释再认一遍(函数名和括号之间夹着注释),自己建的同类函数、视图
|
||||
认不出;information_schema、pg_catalog 里只有表结构的视图不算(核查 agent 查列名、类型常用)。不是查询的语句里,
|
||||
只读表结构的 SHOW(表、列、索引、库、表状态、建库语句)、DESCRIBE 一张表照常,别的 SHOW(ENGINE INNODB STATUS、
|
||||
|
||||
+139
-33
@@ -25,7 +25,9 @@
|
||||
不算(_value_columns、_band_only)。认不全的写法整份快照的列都算,不再逐个形状补规则:认不出落在哪一列的(PIVOT、
|
||||
UNPIVOT 这类也算),(表达式).* 展开读到遮罩列的(复合类型的列、函数交回的记录,外面套了括号、起了别名的也算,展开成
|
||||
几列、叫什么说不清;不只选出项,VALUES、行构造器、函数参数里的也算;Postgres 里 FROM 中可能交回复合类型的表值函数
|
||||
同理,_spreads_record),语法树和实际执行的对不上的(OPAQUE_SQL:MySQL 会执行的注释、U&"…";没有 * 的一支选出项
|
||||
同理,_spreads_record),VALUES 当一张表用(FROM、JOIN、LATERAL 里的,WITH 里的一段,UNION 的一支)又读到遮罩列的
|
||||
(各库给它的列起名不一样:Postgres、SQLite 叫 column1…,MySQL 叫 column_0…,MariaDB 拿第一行的值当列名,
|
||||
_values_names),语法树和实际执行的对不上的(OPAQUE_SQL:MySQL 会执行的注释、U&"…";没有 * 的一支选出项
|
||||
和结果的列数对不上),表名、SQL 写在字符串里交给函数、统计信息和会话记录里存着值的(INDIRECT_FUNCTIONS、
|
||||
INDIRECT_NAMES:query_to_xml 这一族、dblink、pg_stats、MySQL 的 column_statistics、INNODB_TRX、Oracle 的直方图和
|
||||
V$SQL……,可验证报告用的同一份),不只读表结构的 SHOW、EXPLAIN 一条语句(_reads_beyond:引擎状态、日志、出错信息、
|
||||
@@ -862,10 +864,23 @@ def _function_sources(select: Any) -> list[Any]:
|
||||
return out
|
||||
|
||||
|
||||
def _function_columns(source: Any) -> set[str] | None:
|
||||
def _function_node(source: Any) -> Any:
|
||||
"""表值函数这类来源里真正出行的那个节点(函数、VALUES、JSON_TABLE……):套在外面的 LATERAL、表、括号先去掉。"""
|
||||
from sqlglot import exp
|
||||
|
||||
node = source.this if isinstance(source, exp.Lateral) else source
|
||||
func = node.this if isinstance(node, exp.Table) else node
|
||||
while isinstance(func, exp.Subquery):
|
||||
func = func.this
|
||||
return func
|
||||
|
||||
|
||||
def _function_columns(source: Any, kind: str | None = None) -> set[str] | None:
|
||||
"""表值函数这类来源出的列名(小写):写的列名表、别名(Postgres 里一列的函数出的列就叫别名,没起别名的叫函数名)、
|
||||
函数名、常见的出列名(_FUNCTION_COLUMNS);JSON_TABLE 按 COLUMNS 里写的,VALUES 按 column1、column2…。不认得的函数
|
||||
又没写列名表的,说不清出哪几列,为 None。"""
|
||||
函数名、常见的出列名(_FUNCTION_COLUMNS);JSON_TABLE 按 COLUMNS 里写的,VALUES 按数据源的类型 kind 认(_values_names:
|
||||
Postgres、SQLite 叫 column1…,MySQL 叫 column_0…)。不认得的函数又没写列名表的、VALUES 的列名认不准又没写列名表的
|
||||
(MariaDB、Oracle),说不清出哪几列,为 None。kind 没给的按 Postgres、SQLite 的叫法:_has_column 只在 Postgres 的
|
||||
整行引用、(s).* 里要知道来源出不出某一列。"""
|
||||
from sqlglot import exp
|
||||
|
||||
node = source.this if isinstance(source, exp.Lateral) else source
|
||||
@@ -877,16 +892,14 @@ def _function_columns(source: Any) -> set[str] | None:
|
||||
names.add(alias.name.lower())
|
||||
listed = [c.name.lower() for c in alias.args.get("columns") or [] if c.name]
|
||||
names |= set(listed)
|
||||
func = node.this if isinstance(node, exp.Table) else node
|
||||
while isinstance(func, exp.Subquery):
|
||||
func = func.this
|
||||
func = _function_node(source)
|
||||
if isinstance(func, exp.JSONTable):
|
||||
schema = func.args.get("schema")
|
||||
defined = [str(c.name).lower() for c in (schema.expressions if schema is not None else []) if c.name]
|
||||
return names | set(defined) if defined or listed else None
|
||||
if isinstance(func, exp.Values):
|
||||
width = max((len(row.expressions) for row in func.expressions if isinstance(row, exp.Tuple)), default=1)
|
||||
return names | {f"column{i}" for i in range(1, width + 1)}
|
||||
found = _values_names(func, kind, listed)
|
||||
return names | {n for n in found if n} if listed or all(found) else None
|
||||
name = (func.name if isinstance(func, exp.Anonymous) else func.sql_name() if isinstance(func, exp.Func)
|
||||
else "").lower()
|
||||
if name:
|
||||
@@ -894,6 +907,85 @@ def _function_columns(source: Any) -> set[str] | None:
|
||||
return names if listed or name in _TABLE_FUNCTIONS else None
|
||||
|
||||
|
||||
def _values_rows(values: Any, kind: str | None) -> list[list[Any]]:
|
||||
"""一个 VALUES 的每一行按列摊开的各项。MySQL 的一行写作 ROW(…),解析器读成一个元组里套着一个 ROW 函数,摊开它的
|
||||
参数;Postgres 的 VALUES (ROW(…)) 是一列复合类型,不摊。"""
|
||||
from sqlglot import exp
|
||||
|
||||
from app.data.sql_semantics import _read
|
||||
|
||||
mysql = _read(kind) == "mysql"
|
||||
rows: list[list[Any]] = []
|
||||
for row in values.expressions:
|
||||
items = list(row.expressions) if isinstance(row, exp.Tuple) else [row]
|
||||
if mysql and len(items) == 1 and isinstance(items[0], exp.Anonymous) and items[0].name.upper() == "ROW":
|
||||
items = list(items[0].expressions)
|
||||
rows.append(items)
|
||||
return rows
|
||||
|
||||
|
||||
def _values_names(values: Any, kind: str | None, listed: list[str]) -> list[str | None]:
|
||||
"""一个 VALUES 按次序的列名(小写):写了列名表的前几列按列名表(Postgres 的列名表可以只写前几列),后面的按各库的
|
||||
叫法——Postgres、SQLite 叫 column1、column2…,MySQL 8.0 叫 column_0、column_1…(从 0 数起);MariaDB 拿第一行各项的
|
||||
写法当列名,Oracle 的文档只有写了列名表的写法,认不准,记 None。kind 是数据源的类型(sqlite / postgresql / mysql /
|
||||
mariadb / oracle)或解析用的方言名,没给的按 Postgres、SQLite 的叫法。以前一律按 column1… 认,MySQL 的 column_0 对不上,
|
||||
外层按名字取、照原名带出的号码原样交了出去。"""
|
||||
from app.data.sql_semantics import _read
|
||||
|
||||
width = max((len(row) for row in _values_rows(values, kind)), default=1)
|
||||
read = _read(kind)
|
||||
if (kind or "").strip().lower() == "mariadb" or read == "oracle":
|
||||
defaults: list[str | None] = [None] * width
|
||||
elif read == "mysql":
|
||||
defaults = [f"column_{i}" for i in range(width)]
|
||||
else:
|
||||
defaults = [f"column{i}" for i in range(1, width + 1)]
|
||||
return [*listed, *defaults[len(listed):]]
|
||||
|
||||
|
||||
def _values_listed(values: Any) -> list[str]:
|
||||
"""一个 VALUES 写的列名表(小写):写在它自己、套在外面的括号或 LATERAL 上((VALUES …) AS v(a, b))。WITH v(a, b) 的
|
||||
列名表记在 WITH 的那一段上,按派生来源认(_source_names),不在这里。"""
|
||||
from sqlglot import exp
|
||||
|
||||
node, alias = values, values.args.get("alias")
|
||||
while alias is None and isinstance(node.parent, (exp.Subquery, exp.Lateral)) and node.arg_key == "this":
|
||||
node = node.parent
|
||||
alias = node.args.get("alias")
|
||||
return [c.name.lower() for c in alias.args.get("columns") or [] if c.name] if alias is not None else []
|
||||
|
||||
|
||||
def _values_table(values: Any) -> bool:
|
||||
"""这个 VALUES 当一张表用,交出的是行、外层按列名取或照原名带出:FROM、JOIN、LATERAL 里的,WITH 里的一段、UNION 这类
|
||||
的一支(不套括号的解析器改写成 SELECT * FROM (VALUES …) AS _values,套了括号的原样挂着),整条就是它。标量子查询、
|
||||
IN / ANY / EXISTS 里的不算:值经外层的表达式带出(_value_columns 已经算上),或者只用来筛行。"""
|
||||
from sqlglot import exp
|
||||
|
||||
node = values
|
||||
while isinstance(node.parent, exp.Subquery):
|
||||
node = node.parent
|
||||
return node.parent is None or isinstance(node.parent, (exp.From, exp.Join, exp.Lateral, exp.CTE, exp.SetOperation))
|
||||
|
||||
|
||||
def _values_taken(values: Any, names: set[str], kind: str | None) -> set[str]:
|
||||
"""一个 VALUES 里值取自 names 的那几列的列名(_values_names):按位置,任何一行的那一项取自 names 就算(同 UNION 的
|
||||
几支)。一行里 (表达式).*、s.* 展开成几列说不清,它和排在后面的项落在第几列认不出,不算;列名认不准的(None)不算。
|
||||
这些名字照传下去(外层取它们的列、SQL 里拿它们做比较的字面量都按指代遮罩列认),整份遮时也只收这几列的值:同一行里
|
||||
没碰遮罩列的区域不收(以前 VALUES 出的每一列都算,区域名在同一次核查里处处被换掉)。"""
|
||||
from sqlglot import exp
|
||||
|
||||
labels = _values_names(values, kind, _values_listed(values))
|
||||
out: set[str] = set()
|
||||
for row in _values_rows(values, kind):
|
||||
for i, item in enumerate(row):
|
||||
node = _unwrapped(item)
|
||||
if _is_star(node) or (isinstance(node, exp.Dot) and isinstance(node.expression, exp.Star)):
|
||||
break
|
||||
if i < len(labels) and labels[i] and _reads(names, _value_columns(item)):
|
||||
out.add(labels[i])
|
||||
return out
|
||||
|
||||
|
||||
def _spreads_record(source: Any) -> bool:
|
||||
"""Postgres 里这个表值函数这类来源可能交回复合类型、展开成它的各个字段(字段名 SQL 里看不出,同 (表达式).*):
|
||||
- unnest:数组的元素是复合类型时展开成各个字段(unnest(ARRAY[p.card]) 出 addr、tel),元素是什么类型 SQL 里看不出;
|
||||
@@ -904,9 +996,7 @@ def _spreads_record(source: Any) -> bool:
|
||||
|
||||
node = source.this if isinstance(source, exp.Lateral) else source
|
||||
alias = source.args.get("alias") or (node.args.get("alias") if node is not source else None)
|
||||
func = node.this if isinstance(node, exp.Table) else node
|
||||
while isinstance(func, exp.Subquery):
|
||||
func = func.this
|
||||
func = _function_node(source)
|
||||
if isinstance(func, (exp.Values, exp.JSONTable)):
|
||||
return False
|
||||
name = (func.name if isinstance(func, exp.Anonymous) else func.sql_name() if isinstance(func, exp.Func)
|
||||
@@ -1495,9 +1585,11 @@ def _tainted(sql: str, kind: str | None, masked: set[str], width: int, tables: d
|
||||
位置是给没起别名的表达式用的(UPPER(buyer_phone) 的结果列名对不上任何名字):和 * 写在同一支里的,* 前面的从头数、
|
||||
后面的从末尾倒着数。认不出的——两个 * 之间的项、派生来源里没起别名的表达式(外层的 * 会把它原样带出来,列名
|
||||
又认不准)——整份快照的列都算(同解析不了)。
|
||||
FROM、JOIN、LATERAL 里的表值函数(json_each、unnest、JSON_TABLE、LATERAL VALUES)参数里写着这些名字的,它出的列
|
||||
都算(_function_columns):遮罩列是 JSON 时,json_each 拆开是最自然的写法。出哪几列说不清的整份快照都算,Postgres
|
||||
里可能交回复合类型、展开成各个字段的(unnest、只写了列名的列名表,_spreads_record)也算。
|
||||
FROM、JOIN、LATERAL 里的表值函数(json_each、unnest、JSON_TABLE)参数里写着这些名字的,它出的列都算
|
||||
(_function_columns):遮罩列是 JSON 时,json_each 拆开是最自然的写法。出哪几列说不清的整份快照都算,Postgres
|
||||
里可能交回复合类型、展开成各个字段的(unnest、只写了列名的列名表,_spreads_record)也算。VALUES 当一张表用(FROM、
|
||||
JOIN、LATERAL 里的,WITH 里的一段,UNION 的一支,_values_table)读到这些名字的也整份算:各库给它的列起名不一样
|
||||
(_values_names),不逐个方言、逐个位置认;值取自这些名字的那几列,按方言认得出名字的照传下去(_values_taken)。
|
||||
* 展开的列换了名字(写了列名表的派生来源、UNION 后一支的 *):* 带出这些名字的值时,它展开落到的那几列照位置算
|
||||
(_star_places,展开数据源里的表的按 tables 数位置,数不清的整段算);标量子查询里的 *、函数参数里的 *(to_json(s.*)、
|
||||
JSON_OBJECT(*))、整行引用(row_to_json(s))按 _value_columns 算进外层的表达式。表的列名表、TABLE x、套了括号的 *
|
||||
@@ -1538,12 +1630,20 @@ def _tainted_in(root: Any, sql: str, kind: str | None, masked: set[str], width:
|
||||
selects = list(root.find_all(exp.Select))
|
||||
blocks = _blocks(root)
|
||||
sources = [(top, listed) for top, role, listed in blocks if role == "source"]
|
||||
functions = [source for select_ in selects for source in _function_sources(select_)]
|
||||
# VALUES 不按表值函数认(下面另认):按出列名认时它出的每一列都算,同一行里没碰遮罩列的区域也收成了遮罩值
|
||||
functions = [source for select_ in selects for source in _function_sources(select_)
|
||||
if not isinstance(_function_node(source), exp.Values)]
|
||||
# Postgres 里可能交回复合类型、展开成各个字段的表值函数(_spreads_record):读到遮罩列时出哪几列说不清,整份遮
|
||||
spreading = {id(source) for source in functions if _spreads_record(source)} \
|
||||
if _read(kind) in _ROW_DIALECTS else set()
|
||||
unreadable = False
|
||||
for _ in range(len(selects) + len(sources) + len(functions) + 1):
|
||||
# VALUES 当一张表用(FROM、JOIN、LATERAL 里的,WITH 里的一段,UNION 的一支,整条就是它,_values_table)又读到遮罩列
|
||||
# (含派生出来的名字):各库给它的列起名不一样(Postgres、SQLite 叫 column1…,MySQL 叫 column_0…,MariaDB 拿第一行的
|
||||
# 值当列名),外层按名字取、照原名带出时对不上,以前 MySQL 派生表、CTE、LATERAL 里的 VALUES ROW 取了号码,原样交了
|
||||
# 出去;套了括号的 VALUES(整条就是它、UNION 的一支)不在任何 SELECT 的 FROM 里,哪个库都没认。不再逐个方言、逐个
|
||||
# 位置认,整份遮;按方言认得出的、值取自遮罩列的那几列的名字照传下去(_values_taken),整份遮时只收它们的值
|
||||
tables_of_values = [values for values in root.find_all(exp.Values) if _values_table(values)]
|
||||
unreadable = tabled = False
|
||||
for _ in range(len(selects) + len(sources) + len(functions) + len(tables_of_values) + 1):
|
||||
before = len(names)
|
||||
for select_ in selects:
|
||||
for item in select_.expressions:
|
||||
@@ -1554,21 +1654,24 @@ def _tainted_in(root: Any, sql: str, kind: str | None, masked: set[str], width:
|
||||
names |= _source_names(top, listed, names, tables)[0]
|
||||
for source in functions:
|
||||
if _reads(names, _value_columns(source)):
|
||||
columns = None if id(source) in spreading else _function_columns(source)
|
||||
columns = None if id(source) in spreading else _function_columns(source, kind)
|
||||
unreadable = unreadable or columns is None
|
||||
names |= columns or set()
|
||||
for values in tables_of_values:
|
||||
if _reads(names, _value_columns(values)):
|
||||
tabled = True
|
||||
names |= _values_taken(values, names, kind)
|
||||
if len(names) == before:
|
||||
break
|
||||
# 认不出的整份遮:表值函数出哪几列说不清、派生来源里有认不出名字的列、PIVOT 这类读到遮罩列,(表达式).* 读到遮罩列
|
||||
# (展开成几列、叫什么都说不清),语法树和实际执行的对不上、SQL 写在字符串里交给函数执行、整表导出、统计信息里的值
|
||||
# (_opaque)。(表达式).* 不只看选出项(那几个 _normalized 已经换掉,值取自哪些列在 opaque 里):VALUES 里同样展开成
|
||||
# 好几列(Postgres 文档:选出列表、VALUES、行构造器里的 composite.* 都展开),VALUES 里的一项是 s.*、(s.*) 的也一样;
|
||||
# LATERAL VALUES 按表值函数认时一个元组项只算一列,column2 起的号码原样交了出去。不逐个位置认:(表达式).* 在树上任何
|
||||
# 位置(行构造器、函数参数里的不展开成几列,也一样,多遮)、VALUES 里的 *,读到遮罩列就整份遮
|
||||
# 认不出的整份遮:表值函数出哪几列说不清、VALUES 当一张表用(见上)、派生来源里有认不出名字的列、PIVOT 这类读到遮罩列,
|
||||
# (表达式).* 读到遮罩列(展开成几列、叫什么都说不清),语法树和实际执行的对不上、SQL 写在字符串里交给函数执行、整表
|
||||
# 导出、统计信息里的值(_opaque)。(表达式).* 不只看选出项(那几个 _normalized 已经换掉,值取自哪些列在 opaque 里):
|
||||
# VALUES 里同样展开成好几列(Postgres 文档:选出列表、VALUES、行构造器里的 composite.* 都展开),LATERAL VALUES 按表值
|
||||
# 函数认时一个元组项只算一列,column2 起的号码原样交了出去。不逐个位置认:(表达式).* 在树上任何位置(行构造器、函数
|
||||
# 参数里的不展开成几列,也一样,多遮)读到遮罩列就整份遮;VALUES 里的 *、(s.*) 读到遮罩列的,那个 VALUES 就读到了
|
||||
# 遮罩列,按上面整份遮
|
||||
expanded = [node.this for node in root.find_all(exp.Dot) if isinstance(node.expression, exp.Star)]
|
||||
expanded += [item for values in root.find_all(exp.Values) for row in values.expressions
|
||||
if isinstance(row, exp.Tuple) for item in row.expressions if _is_star(_unwrapped(item))]
|
||||
everything = unreadable or any(_source_names(top, listed, names, tables)[1] for top, listed in sources) \
|
||||
everything = unreadable or tabled or any(_source_names(top, listed, names, tables)[1] for top, listed in sources) \
|
||||
or any(_reads(names, _value_columns(node)) for node in root.find_all(exp.Pivot, exp.MatchRecognize)) \
|
||||
or any(_reads(names, cols) for cols in opaque) \
|
||||
or any(_reads(names, _value_columns(node)) for node in expanded) or _opaque(sql, kind)
|
||||
@@ -1641,7 +1744,10 @@ INDIRECT_FUNCTIONS = (
|
||||
#: - 别的会话执行过的 SQL 原文:Postgres 的 pg_stat_activity、pg_stat_statements、本连接预备过的语句;MySQL 的 general_log、
|
||||
#: slow_log、PROCESSLIST、正在执行的事务(information_schema.INNODB_TRX 的 trx_query)、performance_schema(语句历史
|
||||
#: events_statements_*、线程正在执行的 SQL),sys 库里建在 performance_schema 上的会话、锁等待视图(只认这几张,Oracle
|
||||
#: 常写的 sys.dual 不算;带 x$ 的不排版的那一份按下面的 X$ 算);MariaDB 的查询缓存(QUERY_CACHE_INFO);Oracle 的动态视图
|
||||
#: 常写的 sys.dual 不算;带 x$ 的不排版的那一份按下面的 X$ 算)和读 performance_schema 的函数(按 sys.ps_ 开头认:
|
||||
#: ps_thread_trx_info 交回那个线程的事务里执行过的语句原文,ps_thread_stack 交回它的语句栈,MySQL 8.0 实测带着条件里
|
||||
#: 的号码;同一族只交回编号、开关的 ps_thread_id、ps_is_* 也算进来,多遮。不写 sys. 的要数据源的默认库就是 sys 才
|
||||
#: 执行得了,不认,同上面的视图);MariaDB 的查询缓存(QUERY_CACHE_INFO);Oracle 的动态视图
|
||||
#: V$ / GV$ / X$ 一律算,不逐个认(V$SQL 一族、v$sql_bind_capture、打开着的游标、LogMiner 的 V$LOGMNR_CONTENTS、XML 的
|
||||
#: 审计记录都在里面),AWR 历史 DBA_HIST_*、审计记录,SQL 调优集里的语句和绑定变量(*_sqlset_*)、调优顾问记下的
|
||||
#: (*_sqltune_*)、SQL Access Advisor 的工作负载(*_advisor_sqlw_*)、固定下来的执行计划和 SQL Profile、Patch
|
||||
@@ -1659,7 +1765,7 @@ INDIRECT_NAMES = (
|
||||
r"pg_stat_activity", r"pg_stat_statements", r"pg_prepared_statements", r"general_log", r"slow_log",
|
||||
r"processlist", r"innodb_trx", r"innodb_locks", r"query_cache_info", r"optimizer_trace", r"performance_schema",
|
||||
r"events_statements_\w+",
|
||||
r"sys[\"`]?\s*\.\s*[\"`]?(?:x\$)?(?:session|processlist|innodb_lock_waits|schema_table_lock_waits)",
|
||||
r"sys[\"`]?\s*\.\s*[\"`]?(?:(?:x\$)?(?:session|processlist|innodb_lock_waits|schema_table_lock_waits)|ps_\w+)",
|
||||
r"(?:g?v|x)\$\w+", r"(?:dba|cdb)_hist_\w+",
|
||||
r"(?:all|dba|user|cdb)_(?:sqlset|sqltune|advisor_sqlw)_\w+", r"(?:dba|cdb)_sql_(?:plan_baselines|profiles|patches)",
|
||||
r"flashback_transaction_query",
|
||||
@@ -2707,9 +2813,9 @@ def mask_query_result(payload: dict[str, Any], masked: Iterable[str], kind: str
|
||||
SELECT *(展开的列换了名字的也算)、表值函数、整行引用和函数参数里的 *、列名表、TABLE x;COUNT、只在条件里用到、
|
||||
分档当分组键的不算,分档写在派生来源里、外层按它分组的也不算);认不全的(SQL 解析不了又写着遮罩列、*、TABLE x,
|
||||
或者是 Postgres 的;(表达式).* 读到遮罩列(VALUES 里的也算)、Postgres 里可能交回复合类型的表值函数读到遮罩列,
|
||||
MySQL 会执行的注释、U&"…",表名、SQL 写在字符串里交给函数,统计信息、会话记录里存着值的视图,不只读表结构的
|
||||
SHOW、EXPLAIN 一条语句,选出项和结果的列数对不上,分析出意外、超出工作量上限)整份结果的列都遮
|
||||
(QueryMask.whole)。这几列的格子换成「已遮罩」,类型记成文本
|
||||
VALUES 当一张表用又读到遮罩列,MySQL 会执行的注释、U&"…",表名、SQL 写在字符串里交给函数,统计信息、会话记录里
|
||||
存着值的视图和函数,不只读表结构的 SHOW、EXPLAIN 一条语句,选出项和结果的列数对不上,分析出意外、超出工作量
|
||||
上限)整份结果的列都遮(QueryMask.whole)。这几列的格子换成「已遮罩」,类型记成文本
|
||||
- 别的列按值换,只换文字格:整格就是遮罩值的换掉,文字格里夹着的那一截换掉(换了写法的号码也认,
|
||||
_Secrets.loose_spans),换过的格子记下来(QueryMask.cells)。遮罩值是这份结果里的加上 known:先查号码、再查写着
|
||||
号码的备注、没设遮罩的联系方式表(别的库里的也一样)时,后一份同样遮好。门槛同按值遮(SECRET_DIGITS、SECRET_MIN);
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
(ENGINE INNODB STATUS、BINLOG / RELAYLOG EVENTS、REPLICA STATUS、WARNINGS、CREATE TABLE / VIEW……)和 EXPLAIN 一条
|
||||
语句。以前工具边界的名单里没有,可验证报告那份整个挡 information_schema、不附带解析不了的快照,反而挡得住
|
||||
|
||||
复核第 3 轮补的同一类:
|
||||
- VALUES 当一张表用(FROM、JOIN、LATERAL 里的,WITH 里的一段,UNION 的一支)又读到遮罩列(含派生出来的名字):各库给
|
||||
VALUES 的列起名不一样(Postgres、SQLite 叫 column1…,MySQL 8.0 叫 column_0…,MariaDB 拿第一行的值当列名),以前一律
|
||||
按 column1… 认,MySQL 派生表、CTE、LATERAL 里的 VALUES ROW 取了号码,原样交给模型、落库;套了括号的 VALUES(整条
|
||||
就是它、UNION 的一支)解析器原样挂在树上,哪个库都漏。现在不论怎么起名都整份遮;列名按方言认,整份遮时只收值取自
|
||||
遮罩列的那几列的值
|
||||
- MySQL sys 库里读 performance_schema 的函数(sys.ps_thread_trx_info 交回线程的事务里执行过的语句原文,ps_thread_stack
|
||||
同类):按 sys.ps_ 开头认,工具边界和可验证报告共用一份名单
|
||||
|
||||
这里没有真的数据库:结果按它交回的样子造(复合类型展开成几列、SHOW 交回的几列,照 Postgres 18、MySQL 8.0 实测的样子)。
|
||||
数据都是虚构的:手机号、地址、区域。
|
||||
"""
|
||||
@@ -93,21 +102,19 @@ def test_an_expression_star_inside_values_masks_the_whole_result(sql, columns, r
|
||||
assert done.payload["rows"] == [[MASKED_WORD] * len(columns)] and not _leaks(done)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql, columns, row, hidden", [
|
||||
@pytest.mark.parametrize("sql, columns, row", [
|
||||
pytest.param("SELECT v.* FROM people p, LATERAL (VALUES ((p.address).*)) v", ["column1", "column2"],
|
||||
["某路", "某市"], [], id="没设遮罩的复合类型"),
|
||||
pytest.param("SELECT v.column1 AS r FROM sales s, LATERAL (VALUES (s.region)) v", ["r"], ["华东"], [],
|
||||
["某路", "某市"], id="没设遮罩的复合类型"),
|
||||
pytest.param("SELECT v.column1 AS r FROM sales s, LATERAL (VALUES (s.region)) v", ["r"], ["华东"],
|
||||
id="没碰遮罩列的 VALUES"),
|
||||
pytest.param("SELECT v.column1 AS p FROM sales s, LATERAL (VALUES (s.buyer_phone)) v", ["p"], [PHONE], ["p"],
|
||||
id="VALUES 里直接写遮罩列"),
|
||||
pytest.param("SELECT v.column2 AS r FROM (SELECT id, region FROM sales) q, LATERAL (VALUES (q.*)) v", ["r"],
|
||||
["华东"], [], id="没取遮罩列的子查询的 *"),
|
||||
["华东"], id="没取遮罩列的子查询的 *"),
|
||||
])
|
||||
def test_values_without_an_expression_star_on_a_masked_column_is_read_as_before(sql, columns, row, hidden):
|
||||
"""VALUES 里没有读到遮罩列的 (表达式).*:照旧按列认,没碰遮罩列的照常,直接写遮罩列的只遮那一列。"""
|
||||
def test_values_that_reads_no_masked_column_is_read_as_before(sql, columns, row):
|
||||
"""VALUES 没读到遮罩列(没设遮罩的复合类型、没碰遮罩列的列、没取遮罩列的子查询的 *):照常,一列都不遮。读到遮罩列的
|
||||
(直接写遮罩列的也算)当一张表用就整份遮,见下一节。"""
|
||||
done = masked(sql, columns, [list(row)])
|
||||
assert not done.whole and done.columns == hidden
|
||||
assert done.payload["rows"] == [[MASKED_WORD if c in hidden else v for c, v in zip(columns, row)]]
|
||||
assert not done.whole and done.columns == [] and done.payload["rows"] == [list(row)]
|
||||
|
||||
|
||||
def test_an_expression_star_inside_a_function_argument_masks_the_whole_result_too():
|
||||
@@ -117,6 +124,142 @@ def test_an_expression_star_inside_a_function_argument_masks_the_whole_result_to
|
||||
assert done.whole and done.payload["rows"] == [[MASKED_WORD, MASKED_WORD]] and not _leaks(done)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# VALUES 当一张表用(复核第 3 轮)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
#: VALUES 当一张表用、读到遮罩列:(方言, SQL, 结果列, 各行),号码在最后一列。MySQL 8.0 给 VALUES 的列起名 column_0、
|
||||
#: column_1…(从 0 数起),MariaDB 拿第一行的值当列名;以前一律按 column1… 认,MySQL、MariaDB 的这几种号码原样交了出去
|
||||
VALUES_TABLES = [
|
||||
pytest.param("mysql", "SELECT * FROM (VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) AS v", ["column_0"],
|
||||
[[PHONE]], id="MySQL-派生表"),
|
||||
pytest.param("mysql", "SELECT x.column_0 FROM sales s, LATERAL (VALUES ROW(s.buyer_phone)) AS x", ["column_0"],
|
||||
[[PHONE]], id="MySQL-LATERAL"),
|
||||
pytest.param("mysql", "WITH v AS (VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) SELECT * FROM v",
|
||||
["column_0"], [[PHONE]], id="MySQL-CTE"),
|
||||
pytest.param("mysql", "SELECT v.column_0 AS c FROM (VALUES ROW((SELECT buyer_phone FROM sales WHERE id = 1)), "
|
||||
"ROW((SELECT buyer_phone FROM sales WHERE id = 2))) AS v", ["c"], [[PHONE], ["13900000002"]],
|
||||
id="MySQL-外层改了名字"),
|
||||
pytest.param("mysql", "SELECT s.id, t.column_1 FROM sales s JOIN LATERAL (VALUES ROW(s.region, s.buyer_phone)) "
|
||||
"AS t ON TRUE", ["id", "column_1"], [[1, PHONE]], id="MySQL-JOIN LATERAL 连同编号"),
|
||||
pytest.param("mysql", "SELECT w.* FROM sales s, LATERAL (SELECT * FROM (VALUES ROW(s.buyer_phone)) v) AS w",
|
||||
["column_0"], [[PHONE]], id="MySQL-LATERAL 子查询里的 VALUES"),
|
||||
pytest.param("mysql", "SELECT (SELECT column_0 FROM (VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) v) AS p",
|
||||
["p"], [[PHONE]], id="MySQL-标量子查询里的 VALUES"),
|
||||
pytest.param("mysql", "WITH q AS (SELECT buyer_phone AS p FROM sales) "
|
||||
"SELECT v.column_0 FROM q, LATERAL (VALUES ROW(q.p)) AS v", ["column_0"], [[PHONE]],
|
||||
id="MySQL-取派生出来的名字"),
|
||||
pytest.param("mysql", "VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1)) UNION ALL SELECT region FROM sales",
|
||||
["column_0"], [[PHONE], ["华东"]], id="MySQL-UNION 第一支"),
|
||||
# 套了括号的 VALUES 解析器不改写成 FROM 里的 VALUES:整条就是它、UNION 的一支都原样挂在树上,以前不认,Postgres
|
||||
# 里也漏
|
||||
pytest.param("mysql", "(VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1)))", ["column_0"], [[PHONE]],
|
||||
id="MySQL-整条套括号"),
|
||||
pytest.param("postgresql", "(VALUES ((SELECT buyer_phone FROM sales LIMIT 1))) UNION ALL "
|
||||
"(SELECT region FROM sales)", ["column1"], [[PHONE], ["华东"]], id="Postgres-UNION 第一支套括号"),
|
||||
pytest.param("mysql", "SELECT * FROM ((VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) UNION ALL "
|
||||
"(SELECT 'a')) t", ["column_0"], [[PHONE], ["a"]], id="MySQL-派生表里 UNION 的一支"),
|
||||
pytest.param("mariadb", "SELECT * FROM (VALUES ((SELECT buyer_phone FROM sales LIMIT 1))) AS v",
|
||||
["(SELECT buyer_phone FROM sales LIMIT 1)"], [[PHONE]], id="MariaDB-派生表"),
|
||||
pytest.param("mariadb", "SELECT * FROM (VALUES ('华东'), ((SELECT buyer_phone FROM sales LIMIT 1))) AS v",
|
||||
["华东"], [["华东"], [PHONE]], id="MariaDB-第一行的值当列名"),
|
||||
pytest.param("postgresql", "SELECT v.column1 AS p FROM sales s, LATERAL (VALUES (s.buyer_phone)) v", ["p"],
|
||||
[[PHONE]], id="Postgres-LATERAL"),
|
||||
pytest.param("postgresql", "SELECT s.id, v.k, v.val FROM sales s CROSS JOIN LATERAL (VALUES ('phone', "
|
||||
"s.buyer_phone), ('region', s.region)) AS v(k, val)", ["id", "k", "val"],
|
||||
[[1, "phone", PHONE], [1, "region", "华东"]], id="Postgres-行转列"),
|
||||
pytest.param("sqlite", "WITH v AS (VALUES ((SELECT buyer_phone FROM sales LIMIT 1))) SELECT * FROM v",
|
||||
["column1"], [[PHONE]], id="SQLite-CTE"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql, columns, rows", VALUES_TABLES)
|
||||
def test_a_values_table_on_a_masked_column_masks_the_whole_result(kind, sql, columns, rows):
|
||||
"""VALUES 当一张表用、读到遮罩列(含派生出来的名字):不论各库给它的列起什么名字,整份结果的每一格都遮,同一份里
|
||||
没碰遮罩列的编号、区域也遮(多遮)。以前 MySQL、MariaDB 的这几种和套了括号的 VALUES masked=[],号码原样交给模型、
|
||||
落库;Postgres、SQLite 的按列名认得出,只遮了那几列。"""
|
||||
done = masked(sql, columns, [list(r) for r in rows], kind=kind)
|
||||
assert done.whole and done.columns == columns
|
||||
assert done.payload["rows"] == [[MASKED_WORD] * len(columns)] * len(rows) and not _leaks(done)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql, columns, row", [
|
||||
pytest.param("mysql", "SELECT x.column_0 AS r FROM sales s, LATERAL (VALUES ROW(s.region)) AS x", ["r"], ["华东"],
|
||||
id="MySQL-没碰遮罩列"),
|
||||
pytest.param("mysql", "SELECT s.region, b.column_1 AS 档 FROM sales s JOIN (VALUES ROW(0, '小单'), "
|
||||
"ROW(20000, '大单')) AS b ON s.amount >= b.column_0", ["region", "档"], ["华东", "大单"],
|
||||
id="MySQL-写死的分档表"),
|
||||
pytest.param("mysql", "SELECT region FROM sales UNION ALL VALUES ROW('其他')", ["region"], ["其他"],
|
||||
id="MySQL-UNION 一支写死的值"),
|
||||
])
|
||||
def test_a_values_table_reading_no_masked_column_stays_visible(kind, sql, columns, row):
|
||||
"""VALUES 当一张表用、却没读到遮罩列(写死的分档表、没碰遮罩列的 LATERAL、UNION 里写死的一支):照常。"""
|
||||
done = masked(sql, columns, [list(row)], kind=kind)
|
||||
assert not done.whole and done.columns == [] and done.payload["rows"] == [list(row)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql", [
|
||||
# 号码在 ROW(…) 的第二项:MySQL 的一行解析成一个元组里套着一个 ROW 函数,要摊开数位置
|
||||
pytest.param("mysql", "SELECT x.column_1 AS p, x.column_0 AS r FROM sales s, "
|
||||
"LATERAL (VALUES ROW(s.region, s.buyer_phone)) AS x", id="MySQL"),
|
||||
pytest.param("postgresql", "SELECT v.column1 AS p, v.column2 AS r FROM sales s, "
|
||||
"LATERAL (VALUES (s.buyer_phone, s.region)) v", id="Postgres"),
|
||||
pytest.param("postgresql", "SELECT v.a AS p, v.b AS r FROM sales s, LATERAL (VALUES (s.buyer_phone, s.region)) "
|
||||
"v(a, b)", id="Postgres-列名表"),
|
||||
pytest.param("sqlite", "WITH v AS (VALUES ((SELECT buyer_phone FROM sales LIMIT 1), '华东')) "
|
||||
"SELECT column1 AS p, column2 AS r FROM v", id="SQLite-CTE"),
|
||||
])
|
||||
def test_only_the_values_columns_taken_from_a_masked_column_are_collected(kind, sql):
|
||||
"""VALUES 的列名按方言认(MySQL 叫 column_0…,Postgres、SQLite 叫 column1…,写了列名表的按列名表):整份遮时只收值
|
||||
取自遮罩列的那几列的值当遮罩值,后面的查询、回答里同一个号码照样按值换掉;同一行里的区域不收。以前 MySQL 按 column1
|
||||
认,号码一个都没收;Postgres、SQLite 把 VALUES 出的每一列都收了,区域名在同一次核查里处处被换掉。"""
|
||||
done = masked(sql, ["p", "r"], [[PHONE, "华东"]], kind=kind)
|
||||
assert done.whole and done.payload["rows"] == [[MASKED_WORD, MASKED_WORD]]
|
||||
assert PHONE in done.secrets.strong and "华东" not in done.secrets.strong
|
||||
|
||||
|
||||
def test_items_after_an_expanding_star_in_a_values_row_are_not_collected_by_place():
|
||||
"""一行里的 (表达式).* 展开成几列说不清:排在它后面的号码实际落在第三列,按位置数成第二列就收了没设遮罩的城市。
|
||||
整份照遮,这一行不按位置收值(少收不露:这份结果的值本来就没交给模型)。"""
|
||||
done = masked("SELECT v.column2 AS c, v.column3 AS p FROM people p, sales s, "
|
||||
"LATERAL (VALUES ((p.address).*, s.buyer_phone)) v", ["c", "p"], [["某市", PHONE]])
|
||||
assert done.whole and done.payload["rows"] == [[MASKED_WORD, MASKED_WORD]]
|
||||
assert "某市" not in done.secrets.strong
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql, named, unnamed", [
|
||||
pytest.param("mysql", "SELECT * FROM sales s, LATERAL (VALUES ROW(s.id, s.region)) AS x", {"column_0", "column_1"},
|
||||
{"column1", "column2"}, id="MySQL"),
|
||||
pytest.param("postgresql", "SELECT * FROM sales s, LATERAL (VALUES (s.id, s.region)) AS x", {"column1", "column2"},
|
||||
{"column_0"}, id="Postgres"),
|
||||
pytest.param("sqlite", "SELECT * FROM (VALUES (1, '华东')) AS x", {"column1", "column2"}, {"column_0"},
|
||||
id="SQLite"),
|
||||
# 列名表只改前几列的名字:第一列叫 n,不再叫 column1(Postgres 里按 column1 取会报错)
|
||||
pytest.param("postgresql", "SELECT * FROM (VALUES (1, '华东')) AS x(n)", {"n", "column2"}, {"column1"},
|
||||
id="Postgres-列名表只写前几列"),
|
||||
])
|
||||
def test_values_columns_are_named_the_way_each_database_names_them(kind, sql, named, unnamed):
|
||||
"""表值函数这类来源出的列名(_function_columns)里,VALUES 的列按方言起名:别处按列名认来源出不出某一列时对得上。"""
|
||||
from app.data.sql_semantics import _parse, _read
|
||||
|
||||
root, _ = _parse(sql, _read(kind))
|
||||
columns = masking._function_columns(masking._function_sources(root)[-1], kind)
|
||||
assert named <= columns and not unnamed & columns
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql", [
|
||||
pytest.param("mariadb", "SELECT * FROM (VALUES (1, '华东')) AS x", id="MariaDB 拿第一行的值当列名"),
|
||||
pytest.param("oracle", "SELECT * FROM (VALUES (1, '华东')) x", id="Oracle 没写列名表"),
|
||||
])
|
||||
def test_values_columns_that_cannot_be_named_are_unknown(kind, sql):
|
||||
"""VALUES 的列名认不准(MariaDB 拿第一行的值当列名,Oracle 的文档只有写了列名表的写法)又没写列名表:说不清出哪几列。"""
|
||||
from app.data.sql_semantics import _parse, _read
|
||||
|
||||
root, _ = _parse(sql, _read(kind))
|
||||
assert masking._function_columns(masking._function_sources(root)[-1], kind) is None
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 端到端:核查 agent 的真实查询工具
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -178,6 +321,20 @@ async def test_values_expansions_never_reach_the_model_or_the_store(monkeypatch,
|
||||
assert "1390000000" not in json.dumps(snapshot, ensure_ascii=False) and "1390000000" not in api
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql", [
|
||||
"SELECT * FROM (VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) AS v",
|
||||
"SELECT x.column_0 FROM sales s, LATERAL (VALUES ROW(s.buyer_phone)) AS x",
|
||||
"WITH v AS (VALUES ROW((SELECT buyer_phone FROM sales LIMIT 1))) SELECT * FROM v",
|
||||
], ids=["派生表", "LATERAL", "CTE"])
|
||||
async def test_mysql_values_tables_never_reach_the_model_or_the_store(monkeypatch, fake_source, sql):
|
||||
"""端到端(MySQL,VALUES 的列叫 column_0):交给模型的、落库的、通用工件接口给的都只有「已遮罩」,masked_at_query
|
||||
是全部的列,快照记着整份遮。以前 masked_at_query 是空的,号码原样交出。"""
|
||||
seen, snapshot, api = await _asked(monkeypatch, fake_source, "mysql", sql, ["column_0"], [[PHONE], ["13900000002"]])
|
||||
assert MASKED_WORD in seen and "1390000000" not in seen, seen
|
||||
assert snapshot["masked_at_query"] == ["column_0"] and snapshot["masked_whole"] is True
|
||||
assert "1390000000" not in json.dumps(snapshot, ensure_ascii=False) and "1390000000" not in api
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 记着别的会话的 SQL、被锁那一行的值的视图
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -185,6 +342,10 @@ async def test_values_expansions_never_reach_the_model_or_the_store(monkeypatch,
|
||||
|
||||
#: 另一个会话正在执行的语句(号码写在条件里):会话、事务、锁、日志一类的视图交回的就是它
|
||||
BUSY = f"SELECT id, SLEEP(4) FROM sales WHERE buyer_phone = '{PHONE}' FOR UPDATE"
|
||||
#: MySQL sys 库的 ps_thread_trx_info 交回的 JSON:那个线程的事务里执行过的语句原文在 statements_executed[].sql_text 里
|
||||
#: (MySQL 8.0 实测)
|
||||
TRX_INFO = json.dumps([{"trx_id": "1234", "state": "ACTIVE", "statements_executed": [{"sql_text": BUSY}]}],
|
||||
ensure_ascii=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql, kind", [
|
||||
@@ -220,6 +381,32 @@ def test_views_that_keep_other_sessions_sql_mask_the_whole_result(sql, kind):
|
||||
assert done.whole and done.payload["rows"] == [[MASKED_WORD]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql, column", [
|
||||
pytest.param("SELECT sys.ps_thread_trx_info(81) AS t", "t", id="事务里执行过的语句"),
|
||||
pytest.param("SELECT sys.ps_thread_trx_info(81)", "sys.ps_thread_trx_info(81)", id="不起别名"),
|
||||
pytest.param("SELECT `sys`.`ps_thread_trx_info`(81) AS t", "t", id="反引号"),
|
||||
pytest.param("SELECT SYS . PS_THREAD_TRX_INFO (81) AS t", "t", id="大写、隔着空格"),
|
||||
pytest.param("SELECT sys.ps_thread_stack(81, FALSE) AS t", "t", id="语句栈"),
|
||||
pytest.param("SELECT sys.ps_thread_trx_info(sys.ps_thread_id(NULL)) AS t", "t", id="套着取线程号"),
|
||||
])
|
||||
def test_sys_functions_reading_performance_schema_mask_the_whole_result(sql, column):
|
||||
"""MySQL sys 库里读 performance_schema 的函数交回别的会话执行过的 SQL 原文(条件里的号码):按 sys.ps_ 开头认,整份
|
||||
遮。以前名单里 sys 库只认会话、锁等待那几张视图,号码原样交给模型、落库。"""
|
||||
done = masked(sql, [column], [[TRX_INFO]], kind="mysql")
|
||||
assert done.whole and done.payload["rows"] == [[MASKED_WORD]] and not _leaks(done)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind, sql, column, value", [
|
||||
pytest.param("mysql", "SELECT sys.format_bytes(2048) AS b", "b", "2.00 KiB", id="sys 库里只排版的函数"),
|
||||
pytest.param("mysql", "SELECT ps_total AS t FROM stats", "t", 12, id="ps_ 开头的列"),
|
||||
pytest.param("oracle", "SELECT 1 AS n FROM sys.dual", "n", 1, id="Oracle 的 sys.dual"),
|
||||
])
|
||||
def test_other_sys_names_stay_visible(kind, sql, column, value):
|
||||
"""sys 库里别的函数(只排版、换单位的)、不写 sys. 的 ps_ 开头的列名、Oracle 的 sys.dual:照常。"""
|
||||
done = masked(sql, [column], [[value]], kind=kind)
|
||||
assert not done.whole and done.payload["rows"] == [[value]]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql, columns", [
|
||||
pytest.param("SHOW ENGINE INNODB STATUS", ["Type", "Name", "Status"], id="InnoDB 状态"),
|
||||
pytest.param("show engine innodb status", ["Type", "Name", "Status"], id="小写"),
|
||||
@@ -289,7 +476,8 @@ def test_explaining_a_statement_masks_the_whole_result(sql):
|
||||
("SELECT trx_query AS t FROM information_schema.INNODB_TRX", ["t"], [[BUSY]]),
|
||||
("SHOW ENGINE INNODB STATUS", ["Type", "Name", "Status"],
|
||||
[["InnoDB", "", f"---TRANSACTION 1234, ACTIVE 2 sec\nMySQL thread id 9, query id 70 localhost root\n{BUSY}"]]),
|
||||
], ids=["INNODB_TRX", "SHOW ENGINE INNODB STATUS"])
|
||||
("SELECT sys.ps_thread_trx_info(81) AS t", ["t"], [[TRX_INFO]]),
|
||||
], ids=["INNODB_TRX", "SHOW ENGINE INNODB STATUS", "sys.ps_thread_trx_info"])
|
||||
async def test_other_sessions_sql_never_reaches_the_model_or_the_store(monkeypatch, fake_source, sql, columns, rows):
|
||||
"""端到端(MySQL):另一个会话正在执行的 SQL 里写着号码,交给模型的、落库的、通用工件接口给的都只有「已遮罩」,
|
||||
快照记着整份遮。以前 masked_at_query 为空,SQL 原文连同号码原样交出。"""
|
||||
@@ -306,3 +494,18 @@ def test_the_new_views_are_on_the_list_the_bundle_shares():
|
||||
for sql in ("SELECT trx_query FROM information_schema.INNODB_TRX", "SELECT sql_text FROM dba_sqlset_statements",
|
||||
"SELECT undo_sql FROM flashback_transaction_query", "SELECT sql_redo FROM v$logmnr_contents"):
|
||||
assert build._INDIRECT_READS.search(sql) and masking._INDIRECT.search(sql), sql
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sql", [
|
||||
"SELECT sys.ps_thread_trx_info(81) AS t",
|
||||
"SELECT `sys`.`ps_thread_stack`(81, FALSE) AS t",
|
||||
])
|
||||
def test_the_sys_functions_are_on_the_list_the_bundle_shares(sql):
|
||||
"""可验证报告和工具边界共用一份名单:sys.ps_ 开头的函数两边都认,报告不附带这样的快照(只给哈希),它的列整份算取自
|
||||
遮罩列。以前报告判它没碰遮罩列,照常附带快照,别的会话 SQL 里的号码随文件交出去。"""
|
||||
from app.bundle import build
|
||||
|
||||
snapshot = {"source": "shop", "sql": sql, "columns": ["t"], "rows": [[TRX_INFO]]}
|
||||
assert build._INDIRECT_READS.search(sql) and masking._INDIRECT.search(sql)
|
||||
assert build._untouched(snapshot, {"buyer_phone"}) is False
|
||||
assert build._derived_columns(snapshot, {"buyer_phone"}) == {"buyer_phone", "t"}
|
||||
|
||||
Reference in New Issue
Block a user